mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-21 09:14:42 +08:00
feat: init redis storage
This commit is contained in:
@@ -38,7 +38,9 @@
|
||||
"swiper": "^11.2.8",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"vidstack": "^0.6.15",
|
||||
"zod": "^3.24.1"
|
||||
"@upstash/redis": "^1.25.0",
|
||||
"zod": "^3.24.1",
|
||||
"redis": "^4.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^16.3.0",
|
||||
|
||||
97
pnpm-lock.yaml
generated
97
pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ importers:
|
||||
'@heroicons/react':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0(react@18.3.1)
|
||||
'@upstash/redis':
|
||||
specifier: ^1.25.0
|
||||
version: 1.35.1
|
||||
'@vidstack/react':
|
||||
specifier: ^1.12.13
|
||||
version: 1.12.13(@types/react@18.3.23)(react@18.3.1)
|
||||
@@ -53,6 +56,9 @@ importers:
|
||||
react-icons:
|
||||
specifier: ^5.4.0
|
||||
version: 5.5.0(react@18.3.1)
|
||||
redis:
|
||||
specifier: ^4.6.7
|
||||
version: 4.7.1
|
||||
swiper:
|
||||
specifier: ^11.2.8
|
||||
version: 11.2.8
|
||||
@@ -1558,6 +1564,35 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
|
||||
|
||||
'@redis/bloom@1.2.0':
|
||||
resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==}
|
||||
peerDependencies:
|
||||
'@redis/client': ^1.0.0
|
||||
|
||||
'@redis/client@1.6.1':
|
||||
resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@redis/graph@1.1.1':
|
||||
resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==}
|
||||
peerDependencies:
|
||||
'@redis/client': ^1.0.0
|
||||
|
||||
'@redis/json@1.0.7':
|
||||
resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==}
|
||||
peerDependencies:
|
||||
'@redis/client': ^1.0.0
|
||||
|
||||
'@redis/search@1.2.0':
|
||||
resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==}
|
||||
peerDependencies:
|
||||
'@redis/client': ^1.0.0
|
||||
|
||||
'@redis/time-series@1.1.0':
|
||||
resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==}
|
||||
peerDependencies:
|
||||
'@redis/client': ^1.0.0
|
||||
|
||||
'@rollup/plugin-babel@5.3.1':
|
||||
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
@@ -2026,6 +2061,9 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@upstash/redis@1.35.1':
|
||||
resolution: {integrity: sha512-sIMuAMU9IYbE2bkgDby8KLoQKRiBMXn0moXxqLvUmQ7VUu2CvulZLtK8O0x3WQZFvvZhU5sRC2/lOVZdGfudkA==}
|
||||
|
||||
'@vercel/blob@1.0.2':
|
||||
resolution: {integrity: sha512-Im/KeFH4oPx7UsM+QiteimnE07bIUD7JK6CBafI9Z0jRFogaialTBMiZj8EKk/30ctUYsrpIIyP9iIY1YxWnUQ==}
|
||||
engines: {node: '>=16.14'}
|
||||
@@ -2624,6 +2662,10 @@ packages:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
cluster-key-slot@1.1.2:
|
||||
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
co@4.6.0:
|
||||
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
|
||||
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
|
||||
@@ -3640,6 +3682,10 @@ packages:
|
||||
resolution: {integrity: sha512-H7cUpwCQSiJmAHM4c/aFu6fUfrhWXW1ncyh8ftxEPMu6AiYkHw9K8br720TGPZJbk5eOH2bynjZD1yPvdDAmag==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
generic-pool@3.9.0:
|
||||
resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
gensync@1.0.0-beta.2:
|
||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -5181,6 +5227,9 @@ packages:
|
||||
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
redis@4.7.1:
|
||||
resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==}
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -5902,6 +5951,9 @@ packages:
|
||||
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
uncrypto@0.1.3:
|
||||
resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
|
||||
|
||||
undici-types@7.8.0:
|
||||
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
|
||||
|
||||
@@ -7868,6 +7920,32 @@ snapshots:
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
'@redis/bloom@1.2.0(@redis/client@1.6.1)':
|
||||
dependencies:
|
||||
'@redis/client': 1.6.1
|
||||
|
||||
'@redis/client@1.6.1':
|
||||
dependencies:
|
||||
cluster-key-slot: 1.1.2
|
||||
generic-pool: 3.9.0
|
||||
yallist: 4.0.0
|
||||
|
||||
'@redis/graph@1.1.1(@redis/client@1.6.1)':
|
||||
dependencies:
|
||||
'@redis/client': 1.6.1
|
||||
|
||||
'@redis/json@1.0.7(@redis/client@1.6.1)':
|
||||
dependencies:
|
||||
'@redis/client': 1.6.1
|
||||
|
||||
'@redis/search@1.2.0(@redis/client@1.6.1)':
|
||||
dependencies:
|
||||
'@redis/client': 1.6.1
|
||||
|
||||
'@redis/time-series@1.1.0(@redis/client@1.6.1)':
|
||||
dependencies:
|
||||
'@redis/client': 1.6.1
|
||||
|
||||
'@rollup/plugin-babel@5.3.1(@babel/core@7.27.4)(@types/babel__core@7.20.5)(rollup@2.79.2)':
|
||||
dependencies:
|
||||
'@babel/core': 7.27.4
|
||||
@@ -8366,6 +8444,10 @@ snapshots:
|
||||
'@unrs/resolver-binding-win32-x64-msvc@1.9.0':
|
||||
optional: true
|
||||
|
||||
'@upstash/redis@1.35.1':
|
||||
dependencies:
|
||||
uncrypto: 0.1.3
|
||||
|
||||
'@vercel/blob@1.0.2':
|
||||
dependencies:
|
||||
async-retry: 1.3.3
|
||||
@@ -9114,6 +9196,8 @@ snapshots:
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
cluster-key-slot@1.1.2: {}
|
||||
|
||||
co@4.6.0: {}
|
||||
|
||||
code-block-writer@10.1.1: {}
|
||||
@@ -10192,6 +10276,8 @@ snapshots:
|
||||
|
||||
generic-pool@3.4.2: {}
|
||||
|
||||
generic-pool@3.9.0: {}
|
||||
|
||||
gensync@1.0.0-beta.2: {}
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
@@ -11927,6 +12013,15 @@ snapshots:
|
||||
indent-string: 4.0.0
|
||||
strip-indent: 3.0.0
|
||||
|
||||
redis@4.7.1:
|
||||
dependencies:
|
||||
'@redis/bloom': 1.2.0(@redis/client@1.6.1)
|
||||
'@redis/client': 1.6.1
|
||||
'@redis/graph': 1.1.1(@redis/client@1.6.1)
|
||||
'@redis/json': 1.0.7(@redis/client@1.6.1)
|
||||
'@redis/search': 1.2.0(@redis/client@1.6.1)
|
||||
'@redis/time-series': 1.1.0(@redis/client@1.6.1)
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@@ -12737,6 +12832,8 @@ snapshots:
|
||||
has-symbols: 1.1.0
|
||||
which-boxed-primitive: 1.1.1
|
||||
|
||||
uncrypto@0.1.3: {}
|
||||
|
||||
undici-types@7.8.0: {}
|
||||
|
||||
undici@5.28.4:
|
||||
|
||||
153
src/app/api/favorites/route.ts
Normal file
153
src/app/api/favorites/route.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { db, Favorite } from '@/lib/db';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
/**
|
||||
* GET /api/favorites
|
||||
*
|
||||
* 支持两种调用方式:
|
||||
* 1. 不带 query,返回全部收藏列表(Record<string, Favorite>)。
|
||||
* 2. 带 key=source+id,返回单条收藏(Favorite | null)。
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const key = searchParams.get('key');
|
||||
const user = searchParams.get('user');
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Missing user' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 查询单条收藏
|
||||
if (key) {
|
||||
const [source, id] = key.split('+');
|
||||
if (!source || !id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid key format' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const fav = await db.getFavorite(user, source, id);
|
||||
return NextResponse.json(fav, { status: 200 });
|
||||
}
|
||||
|
||||
// 查询全部收藏
|
||||
const favorites = await db.getAllFavorites(user);
|
||||
return NextResponse.json(favorites, { status: 200 });
|
||||
} catch (err) {
|
||||
console.error('获取收藏失败', err);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/favorites
|
||||
* body: { user?: string; key: string; favorite: Favorite }
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const {
|
||||
user,
|
||||
key,
|
||||
favorite,
|
||||
}: { user?: string; key: string; favorite: Favorite } = body;
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Missing user' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!key || !favorite) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing key or favorite' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 验证必要字段
|
||||
if (!favorite.title || !favorite.source_name) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid favorite data' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const [source, id] = key.split('+');
|
||||
if (!source || !id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid key format' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const favoriteWithoutUserId = {
|
||||
...favorite,
|
||||
save_time: favorite.save_time ?? Date.now(),
|
||||
} as Omit<Favorite, 'user_id'>;
|
||||
|
||||
await db.saveFavorite(user, source, id, favoriteWithoutUserId);
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
} catch (err) {
|
||||
console.error('保存收藏失败', err);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/favorites
|
||||
*
|
||||
* 1. 不带 query -> 清空全部收藏
|
||||
* 2. 带 key=source+id -> 删除单条收藏
|
||||
*/
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const key = searchParams.get('key');
|
||||
const user = searchParams.get('user');
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Missing user' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (key) {
|
||||
// 删除单条
|
||||
const [source, id] = key.split('+');
|
||||
if (!source || !id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid key format' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
await db.deleteFavorite(user, source, id);
|
||||
} else {
|
||||
// 清空全部
|
||||
const all = await db.getAllFavorites(user);
|
||||
await Promise.all(
|
||||
Object.keys(all).map(async (k) => {
|
||||
const [s, i] = k.split('+');
|
||||
if (s && i) await db.deleteFavorite(user, s, i);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
} catch (err) {
|
||||
console.error('删除收藏失败', err);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,66 @@
|
||||
/* eslint-disable no-console */
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
// 读取存储类型环境变量,默认 localstorage
|
||||
const STORAGE_TYPE =
|
||||
(process.env.NEXT_PUBLIC_STORAGE_TYPE as string | undefined) ||
|
||||
'localstorage';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const result = process.env.PASSWORD;
|
||||
// 本地 / localStorage 模式——仅校验固定密码
|
||||
if (STORAGE_TYPE === 'localstorage') {
|
||||
const envPassword = process.env.PASSWORD;
|
||||
|
||||
// 未配置 PASSWORD 时直接放行
|
||||
if (!envPassword) return NextResponse.json({ ok: true });
|
||||
|
||||
const { password } = await req.json();
|
||||
if (typeof password !== 'string') {
|
||||
return NextResponse.json({ error: '密码不能为空' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (password !== envPassword) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: '密码错误' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
const { password } = await req.json();
|
||||
if (typeof password !== 'string') {
|
||||
// 数据库 / redis 模式——校验用户名并尝试连接数据库
|
||||
const { username, password } = await req.json();
|
||||
|
||||
if (!username || typeof username !== 'string') {
|
||||
return NextResponse.json({ error: '用户名不能为空' }, { status: 400 });
|
||||
}
|
||||
if (!password || typeof password !== 'string') {
|
||||
return NextResponse.json({ error: '密码不能为空' }, { status: 400 });
|
||||
}
|
||||
|
||||
const matched = password === result;
|
||||
// 校验用户密码
|
||||
try {
|
||||
const pass = await db.verifyUser(username, password);
|
||||
if (!pass) {
|
||||
return NextResponse.json(
|
||||
{ error: '用户名或密码错误' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: '密码错误' },
|
||||
{ status: 401 }
|
||||
);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (err) {
|
||||
console.error('数据库验证失败', err);
|
||||
return NextResponse.json({ error: '数据库错误' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
console.error('登录接口异常', error);
|
||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,15 @@ import { PlayRecord } from '@/lib/db';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const records = await db.getAllPlayRecords();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const user = searchParams.get('user');
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Missing user' }, { status: 400 });
|
||||
}
|
||||
|
||||
const records = await db.getAllPlayRecords(user);
|
||||
return NextResponse.json(records, { status: 200 });
|
||||
} catch (err) {
|
||||
console.error('获取播放记录失败', err);
|
||||
@@ -23,7 +29,15 @@ export async function GET() {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { key, record }: { key: string; record: PlayRecord } = body;
|
||||
const {
|
||||
user,
|
||||
key,
|
||||
record,
|
||||
}: { user?: string; key: string; record: PlayRecord } = body;
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Missing user' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!key || !record) {
|
||||
return NextResponse.json(
|
||||
@@ -61,7 +75,7 @@ export async function POST(request: NextRequest) {
|
||||
save_time: record.save_time,
|
||||
};
|
||||
|
||||
await db.savePlayRecord(source, id, recordWithoutUserId);
|
||||
await db.savePlayRecord(user, source, id, recordWithoutUserId);
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
} catch (err) {
|
||||
@@ -72,3 +86,46 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const key = searchParams.get('key');
|
||||
const user = searchParams.get('user');
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Missing user' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (key) {
|
||||
// 如果提供了 key,删除单条播放记录
|
||||
const [source, id] = key.split('+');
|
||||
if (!source || !id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid key format' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await db.deletePlayRecord(user, source, id);
|
||||
} else {
|
||||
// 未提供 key,则清空全部播放记录
|
||||
// 目前 DbManager 没有对应方法,这里直接遍历删除
|
||||
const all = await db.getAllPlayRecords(user);
|
||||
await Promise.all(
|
||||
Object.keys(all).map(async (k) => {
|
||||
const [s, i] = k.split('+');
|
||||
if (s && i) await db.deletePlayRecord(user, s, i);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
} catch (err) {
|
||||
console.error('删除播放记录失败', err);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
79
src/app/api/searchhistory/route.ts
Normal file
79
src/app/api/searchhistory/route.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
// 最大保存条数(与客户端保持一致)
|
||||
const HISTORY_LIMIT = 20;
|
||||
|
||||
/**
|
||||
* GET /api/searchhistory
|
||||
* 返回 string[]
|
||||
*/
|
||||
export async function GET() {
|
||||
try {
|
||||
const history = await db.getSearchHistory();
|
||||
return NextResponse.json(history, { status: 200 });
|
||||
} catch (err) {
|
||||
console.error('获取搜索历史失败', err);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/searchhistory
|
||||
* body: { keyword: string }
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const keyword: string = body.keyword?.trim();
|
||||
if (!keyword) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Keyword is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await db.addSearchHistory(keyword);
|
||||
|
||||
// 再次获取最新列表,确保客户端与服务端同步
|
||||
const history = await db.getSearchHistory();
|
||||
return NextResponse.json(history.slice(0, HISTORY_LIMIT), { status: 200 });
|
||||
} catch (err) {
|
||||
console.error('添加搜索历史失败', err);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/searchhistory
|
||||
*
|
||||
* 1. 不带 keyword -> 清空全部搜索历史
|
||||
* 2. 带 keyword=<kw> -> 删除单条关键字
|
||||
*/
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const kw = searchParams.get('keyword')?.trim();
|
||||
|
||||
await db.deleteSearchHistory(kw || undefined);
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
} catch (err) {
|
||||
console.error('删除搜索历史失败', err);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -33,8 +33,25 @@ export default function RootLayout({
|
||||
process.env.ANNOUNCEMENT ||
|
||||
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。';
|
||||
|
||||
// 将运行时配置注入到全局 window 对象,供客户端在运行时读取
|
||||
const runtimeConfig = {
|
||||
STORAGE_TYPE:
|
||||
process.env.STORAGE_TYPE ||
|
||||
process.env.NEXT_PUBLIC_STORAGE_TYPE ||
|
||||
'localstorage',
|
||||
};
|
||||
|
||||
return (
|
||||
<html lang='zh-CN' suppressHydrationWarning>
|
||||
<head>
|
||||
{/* 将配置序列化后直接写入脚本,浏览器端可通过 window.RUNTIME_CONFIG 获取 */}
|
||||
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `window.RUNTIME_CONFIG = ${JSON.stringify(runtimeConfig)};`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
className={`${inter.className} min-h-screen bg-white text-gray-900 dark:bg-black dark:text-gray-200`}
|
||||
>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
'use client';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
@@ -10,28 +12,41 @@ function LoginPageClient() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [password, setPassword] = useState('');
|
||||
const [username, setUsername] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { siteName } = useSite();
|
||||
|
||||
// 当 STORAGE_TYPE 不为空且不为 localstorage 时,要求输入用户名
|
||||
const shouldAskUsername =
|
||||
typeof window !== 'undefined' &&
|
||||
(window as any).RUNTIME_CONFIG?.STORAGE_TYPE &&
|
||||
(window as any).RUNTIME_CONFIG?.STORAGE_TYPE !== 'localstorage';
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
if (!password) return;
|
||||
if (!password || (shouldAskUsername && !username)) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password }),
|
||||
body: JSON.stringify({
|
||||
password,
|
||||
...(shouldAskUsername ? { username } : {}),
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
// 保存密码以供后续请求使用
|
||||
// 保存密码和用户名以供后续请求使用
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('password', password);
|
||||
if (shouldAskUsername) {
|
||||
localStorage.setItem('username', username);
|
||||
}
|
||||
}
|
||||
|
||||
const redirect = searchParams.get('redirect') || '/';
|
||||
@@ -57,6 +72,23 @@ function LoginPageClient() {
|
||||
{siteName}
|
||||
</h1>
|
||||
<form onSubmit={handleSubmit} className='space-y-8'>
|
||||
{shouldAskUsername && (
|
||||
<div>
|
||||
<label htmlFor='username' className='sr-only'>
|
||||
用户名
|
||||
</label>
|
||||
<input
|
||||
id='username'
|
||||
type='text'
|
||||
autoComplete='username'
|
||||
className='block w-full rounded-lg border-0 py-3 px-4 text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-white/60 dark:ring-white/20 placeholder:text-gray-500 dark:placeholder:text-gray-400 focus:ring-2 focus:ring-green-500 focus:outline-none sm:text-base bg-white/60 dark:bg-zinc-800/60 backdrop-blur'
|
||||
placeholder='输入用户名'
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor='password' className='sr-only'>
|
||||
密码
|
||||
@@ -78,7 +110,7 @@ function LoginPageClient() {
|
||||
|
||||
<button
|
||||
type='submit'
|
||||
disabled={!password || loading}
|
||||
disabled={!password || loading || (shouldAskUsername && !username)}
|
||||
className='inline-flex w-full justify-center rounded-lg bg-green-600 py-3 text-base font-semibold text-white shadow-lg transition-all duration-200 hover:from-green-600 hover:to-blue-600 disabled:cursor-not-allowed disabled:opacity-50'
|
||||
>
|
||||
{loading ? '登录中...' : '登录'}
|
||||
|
||||
@@ -23,8 +23,9 @@ export default function AuthProvider({ children }: Props) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 从localStorage获取密码
|
||||
// 从localStorage获取密码和用户名
|
||||
const password = localStorage.getItem('password');
|
||||
const username = localStorage.getItem('username');
|
||||
const fullPath =
|
||||
typeof window !== 'undefined'
|
||||
? window.location.pathname + window.location.search
|
||||
@@ -35,7 +36,7 @@ export default function AuthProvider({ children }: Props) {
|
||||
const res = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password }),
|
||||
body: JSON.stringify({ password, username }),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('认证失败');
|
||||
@@ -45,6 +46,7 @@ export default function AuthProvider({ children }: Props) {
|
||||
// 认证失败,清理并跳转登录
|
||||
setIsAuthenticated(false);
|
||||
localStorage.removeItem('password');
|
||||
localStorage.removeItem('username');
|
||||
router.replace(`/login?redirect=${encodeURIComponent(fullPath)}`);
|
||||
}
|
||||
}, [pathname, router]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable no-console, @typescript-eslint/no-explicit-any */
|
||||
'use client';
|
||||
|
||||
/**
|
||||
@@ -30,12 +30,31 @@ export interface PlayRecord {
|
||||
// ---- 常量 ----
|
||||
const PLAY_RECORDS_KEY = 'moontv_play_records';
|
||||
|
||||
// +++ 新增:获取当前用户名工具函数 +++
|
||||
/**
|
||||
* 从 localStorage 中读取当前用户名
|
||||
* 如果不存在则返回 undefined
|
||||
*/
|
||||
function getUsername(): string | undefined {
|
||||
if (typeof window === 'undefined') return undefined;
|
||||
try {
|
||||
const name = localStorage.getItem('username')?.trim();
|
||||
return name || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 环境变量 ----
|
||||
const STORAGE_TYPE =
|
||||
(process.env.NEXT_PUBLIC_STORAGE_TYPE as
|
||||
| 'localstorage'
|
||||
| 'database'
|
||||
| undefined) || 'localstorage';
|
||||
const STORAGE_TYPE = (() => {
|
||||
const raw =
|
||||
(typeof window !== 'undefined' &&
|
||||
(window as any).RUNTIME_CONFIG?.STORAGE_TYPE) ||
|
||||
(process.env.STORAGE_TYPE as 'localstorage' | 'redis' | undefined) ||
|
||||
'localstorage';
|
||||
// 兼容 redis => database
|
||||
return raw;
|
||||
})();
|
||||
|
||||
// ---------------- 搜索历史相关常量 ----------------
|
||||
const SEARCH_HISTORY_KEY = 'moontv_search_history';
|
||||
@@ -64,8 +83,11 @@ export function generateStorageKey(source: string, id: string): string {
|
||||
*/
|
||||
export async function getAllPlayRecords(): Promise<Record<string, PlayRecord>> {
|
||||
// 若配置标明使用数据库,则从后端 API 拉取
|
||||
if (STORAGE_TYPE === 'database') {
|
||||
return fetchFromApi<Record<string, PlayRecord>>('/api/playrecords');
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
const user = getUsername();
|
||||
return fetchFromApi<Record<string, PlayRecord>>(
|
||||
`/api/playrecords?user=${encodeURIComponent(user ?? '')}`
|
||||
);
|
||||
}
|
||||
|
||||
// 默认 / localstorage 流程
|
||||
@@ -96,14 +118,15 @@ export async function savePlayRecord(
|
||||
const fullRecord: PlayRecord = { ...record, user_id: 0 };
|
||||
|
||||
// 若配置标明使用数据库,则通过 API 保存
|
||||
if (STORAGE_TYPE === 'database') {
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
try {
|
||||
const user = getUsername();
|
||||
const res = await fetch('/api/playrecords', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ key, record: fullRecord }),
|
||||
body: JSON.stringify({ user, key, record: fullRecord }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`保存播放记录失败: ${res.status}`);
|
||||
} catch (err) {
|
||||
@@ -139,10 +162,12 @@ export async function deletePlayRecord(
|
||||
const key = generateStorageKey(source, id);
|
||||
|
||||
// 若配置标明使用数据库,则通过 API 删除
|
||||
if (STORAGE_TYPE === 'database') {
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/playrecords?key=${encodeURIComponent(key)}`,
|
||||
`/api/playrecords?key=${encodeURIComponent(
|
||||
key
|
||||
)}&user=${encodeURIComponent(getUsername() ?? '')}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
@@ -179,7 +204,7 @@ export async function deletePlayRecord(
|
||||
*/
|
||||
export async function getSearchHistory(): Promise<string[]> {
|
||||
// 如果配置为使用数据库,则从后端 API 获取
|
||||
if (STORAGE_TYPE === 'database') {
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
try {
|
||||
return fetchFromApi<string[]>('/api/searchhistory');
|
||||
} catch (err) {
|
||||
@@ -213,7 +238,7 @@ export async function addSearchHistory(keyword: string): Promise<void> {
|
||||
if (!trimmed) return;
|
||||
|
||||
// 数据库模式
|
||||
if (STORAGE_TYPE === 'database') {
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
try {
|
||||
await fetch('/api/searchhistory', {
|
||||
method: 'POST',
|
||||
@@ -249,7 +274,7 @@ export async function addSearchHistory(keyword: string): Promise<void> {
|
||||
*/
|
||||
export async function clearSearchHistory(): Promise<void> {
|
||||
// 数据库模式
|
||||
if (STORAGE_TYPE === 'database') {
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
try {
|
||||
await fetch('/api/searchhistory', {
|
||||
method: 'DELETE',
|
||||
@@ -273,7 +298,7 @@ export async function deleteSearchHistory(keyword: string): Promise<void> {
|
||||
if (!trimmed) return;
|
||||
|
||||
// 数据库模式
|
||||
if (STORAGE_TYPE === 'database') {
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
try {
|
||||
await fetch(`/api/searchhistory?keyword=${encodeURIComponent(trimmed)}`, {
|
||||
method: 'DELETE',
|
||||
@@ -317,8 +342,11 @@ const FAVORITES_KEY = 'moontv_favorites';
|
||||
*/
|
||||
export async function getAllFavorites(): Promise<Record<string, Favorite>> {
|
||||
// 数据库模式
|
||||
if (STORAGE_TYPE === 'database') {
|
||||
return fetchFromApi<Record<string, Favorite>>('/api/favorites');
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
const user = getUsername();
|
||||
return fetchFromApi<Record<string, Favorite>>(
|
||||
`/api/favorites?user=${encodeURIComponent(user ?? '')}`
|
||||
);
|
||||
}
|
||||
|
||||
// localStorage 模式
|
||||
@@ -348,14 +376,15 @@ export async function saveFavorite(
|
||||
const fullFavorite: Favorite = { ...favorite, user_id: 0 };
|
||||
|
||||
// 数据库模式
|
||||
if (STORAGE_TYPE === 'database') {
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
try {
|
||||
const user = getUsername();
|
||||
const res = await fetch('/api/favorites', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ key, favorite: fullFavorite }),
|
||||
body: JSON.stringify({ user, key, favorite: fullFavorite }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`保存收藏失败: ${res.status}`);
|
||||
} catch (err) {
|
||||
@@ -391,11 +420,17 @@ export async function deleteFavorite(
|
||||
const key = generateStorageKey(source, id);
|
||||
|
||||
// 数据库模式
|
||||
if (STORAGE_TYPE === 'database') {
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
try {
|
||||
const res = await fetch(`/api/favorites?key=${encodeURIComponent(key)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const user = getUsername();
|
||||
const res = await fetch(
|
||||
`/api/favorites?key=${encodeURIComponent(
|
||||
key
|
||||
)}&user=${encodeURIComponent(user ?? '')}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
if (!res.ok) throw new Error(`删除收藏失败: ${res.status}`);
|
||||
} catch (err) {
|
||||
console.error('删除收藏到数据库失败:', err);
|
||||
@@ -430,9 +465,14 @@ export async function isFavorited(
|
||||
const key = generateStorageKey(source, id);
|
||||
|
||||
// 数据库模式
|
||||
if (STORAGE_TYPE === 'database') {
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
try {
|
||||
const res = await fetch(`/api/favorites?key=${encodeURIComponent(key)}`);
|
||||
const user = getUsername();
|
||||
const res = await fetch(
|
||||
`/api/favorites?key=${encodeURIComponent(
|
||||
key
|
||||
)}&user=${encodeURIComponent(user ?? '')}`
|
||||
);
|
||||
if (!res.ok) return false;
|
||||
const data = await res.json();
|
||||
return !!data;
|
||||
@@ -476,10 +516,12 @@ export async function toggleFavorite(
|
||||
*/
|
||||
export async function clearAllPlayRecords(): Promise<void> {
|
||||
// 数据库模式
|
||||
if (STORAGE_TYPE === 'database') {
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
const user = getUsername();
|
||||
try {
|
||||
await fetch('/api/playrecords', {
|
||||
await fetch(`/api/playrecords?user=${encodeURIComponent(user ?? '')}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('清空播放记录失败:', err);
|
||||
@@ -497,10 +539,12 @@ export async function clearAllPlayRecords(): Promise<void> {
|
||||
*/
|
||||
export async function clearAllFavorites(): Promise<void> {
|
||||
// 数据库模式
|
||||
if (STORAGE_TYPE === 'database') {
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
const user = getUsername();
|
||||
try {
|
||||
await fetch('/api/favorites', {
|
||||
await fetch(`/api/favorites?user=${encodeURIComponent(user ?? '')}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('清空收藏失败:', err);
|
||||
|
||||
307
src/lib/db.ts
307
src/lib/db.ts
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable no-console, @typescript-eslint/no-explicit-any */
|
||||
|
||||
// storage type 常量: 'localstorage' | 'database',默认 'localstorage'
|
||||
const STORAGE_TYPE =
|
||||
(process.env.NEXT_PUBLIC_STORAGE_TYPE as
|
||||
| 'localstorage'
|
||||
| 'database'
|
||||
| 'redis'
|
||||
| undefined) || 'localstorage';
|
||||
|
||||
// 播放记录数据结构
|
||||
@@ -30,71 +30,180 @@ export interface Favorite {
|
||||
save_time: number; // 记录保存时间(时间戳)
|
||||
}
|
||||
|
||||
// 搜索历史最大条数
|
||||
const SEARCH_HISTORY_LIMIT = 20;
|
||||
|
||||
// 存储接口
|
||||
export interface IStorage {
|
||||
// 播放记录相关
|
||||
getPlayRecord(key: string): Promise<PlayRecord | null>;
|
||||
setPlayRecord(key: string, record: PlayRecord): Promise<void>;
|
||||
getAllPlayRecords(): Promise<{ [key: string]: PlayRecord }>;
|
||||
deletePlayRecord(key: string): Promise<void>;
|
||||
getPlayRecord(userName: string, key: string): Promise<PlayRecord | null>;
|
||||
setPlayRecord(
|
||||
userName: string,
|
||||
key: string,
|
||||
record: PlayRecord
|
||||
): Promise<void>;
|
||||
getAllPlayRecords(userName: string): Promise<{ [key: string]: PlayRecord }>;
|
||||
deletePlayRecord(userName: string, key: string): Promise<void>;
|
||||
|
||||
// 收藏相关
|
||||
getFavorite(key: string): Promise<Favorite | null>;
|
||||
setFavorite(key: string, favorite: Favorite): Promise<void>;
|
||||
getAllFavorites(): Promise<{ [key: string]: Favorite }>;
|
||||
deleteFavorite(key: string): Promise<void>;
|
||||
getFavorite(userName: string, key: string): Promise<Favorite | null>;
|
||||
setFavorite(userName: string, key: string, favorite: Favorite): Promise<void>;
|
||||
getAllFavorites(userName: string): Promise<{ [key: string]: Favorite }>;
|
||||
deleteFavorite(userName: string, key: string): Promise<void>;
|
||||
|
||||
// 用户相关
|
||||
registerUser(userName: string, password: string): Promise<void>;
|
||||
verifyUser(userName: string, password: string): Promise<boolean>;
|
||||
|
||||
// 搜索历史相关
|
||||
getSearchHistory(): Promise<string[]>;
|
||||
addSearchHistory(keyword: string): Promise<void>;
|
||||
deleteSearchHistory(keyword?: string): Promise<void>;
|
||||
}
|
||||
|
||||
// 数据库实现(保留接口,待实现)
|
||||
class DatabaseStorage implements IStorage {
|
||||
async getPlayRecord(_key: string): Promise<PlayRecord | null> {
|
||||
// TODO: 实现数据库查询逻辑
|
||||
throw new Error('Database storage not implemented yet');
|
||||
// ---------------- Redis 实现 ----------------
|
||||
import { createClient, RedisClientType } from 'redis';
|
||||
|
||||
class RedisStorage implements IStorage {
|
||||
private client: RedisClientType;
|
||||
|
||||
constructor() {
|
||||
this.client = getRedisClient();
|
||||
}
|
||||
|
||||
async setPlayRecord(_key: string, _record: PlayRecord): Promise<void> {
|
||||
// TODO: 实现数据库插入/更新逻辑
|
||||
throw new Error('Database storage not implemented yet');
|
||||
// ---------- 播放记录 ----------
|
||||
private prKey(user: string, key: string) {
|
||||
return `u:${user}:pr:${key}`; // u:username:pr:source+id
|
||||
}
|
||||
|
||||
async getAllPlayRecords(): Promise<{ [key: string]: PlayRecord }> {
|
||||
// TODO: 实现数据库查询所有记录逻辑
|
||||
throw new Error('Database storage not implemented yet');
|
||||
async getPlayRecord(
|
||||
userName: string,
|
||||
key: string
|
||||
): Promise<PlayRecord | null> {
|
||||
const val = await this.client.get(this.prKey(userName, key));
|
||||
return val ? (JSON.parse(val) as PlayRecord) : null;
|
||||
}
|
||||
|
||||
async deletePlayRecord(_key: string): Promise<void> {
|
||||
// TODO: 实现数据库删除逻辑
|
||||
throw new Error('Database storage not implemented yet');
|
||||
async setPlayRecord(
|
||||
userName: string,
|
||||
key: string,
|
||||
record: PlayRecord
|
||||
): Promise<void> {
|
||||
await this.client.set(this.prKey(userName, key), JSON.stringify(record));
|
||||
}
|
||||
|
||||
async getFavorite(_: string): Promise<Favorite | null> {
|
||||
// TODO: 实现数据库查询逻辑
|
||||
throw new Error('Database storage not implemented yet');
|
||||
async getAllPlayRecords(
|
||||
userName: string
|
||||
): Promise<Record<string, PlayRecord>> {
|
||||
const pattern = `u:${userName}:pr:*`;
|
||||
const keys: string[] = await this.client.keys(pattern);
|
||||
if (keys.length === 0) return {};
|
||||
const values = await this.client.mGet(keys);
|
||||
const result: Record<string, PlayRecord> = {};
|
||||
keys.forEach((fullKey: string, idx: number) => {
|
||||
const raw = values[idx];
|
||||
if (raw) {
|
||||
const rec = JSON.parse(raw) as PlayRecord;
|
||||
// 截取 source+id 部分
|
||||
const keyPart = fullKey.replace(`u:${userName}:pr:`, '');
|
||||
result[keyPart] = rec;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
async setFavorite(_key: string, _favorite: Favorite): Promise<void> {
|
||||
// TODO: 实现数据库插入/更新逻辑
|
||||
throw new Error('Database storage not implemented yet');
|
||||
async deletePlayRecord(userName: string, key: string): Promise<void> {
|
||||
await this.client.del(this.prKey(userName, key));
|
||||
}
|
||||
|
||||
async getAllFavorites(): Promise<{ [key: string]: Favorite }> {
|
||||
// TODO: 实现数据库查询所有收藏逻辑
|
||||
throw new Error('Database storage not implemented yet');
|
||||
// ---------- 收藏 ----------
|
||||
private favKey(user: string, key: string) {
|
||||
return `u:${user}:fav:${key}`;
|
||||
}
|
||||
|
||||
async deleteFavorite(_key: string): Promise<void> {
|
||||
// TODO: 实现数据库删除逻辑
|
||||
throw new Error('Database storage not implemented yet');
|
||||
async getFavorite(userName: string, key: string): Promise<Favorite | null> {
|
||||
const val = await this.client.get(this.favKey(userName, key));
|
||||
return val ? (JSON.parse(val) as Favorite) : null;
|
||||
}
|
||||
|
||||
async setFavorite(
|
||||
userName: string,
|
||||
key: string,
|
||||
favorite: Favorite
|
||||
): Promise<void> {
|
||||
await this.client.set(this.favKey(userName, key), JSON.stringify(favorite));
|
||||
}
|
||||
|
||||
async getAllFavorites(userName: string): Promise<Record<string, Favorite>> {
|
||||
const pattern = `u:${userName}:fav:*`;
|
||||
const keys: string[] = await this.client.keys(pattern);
|
||||
if (keys.length === 0) return {};
|
||||
const values = await this.client.mGet(keys);
|
||||
const result: Record<string, Favorite> = {};
|
||||
keys.forEach((fullKey: string, idx: number) => {
|
||||
const raw = values[idx];
|
||||
if (raw) {
|
||||
const fav = JSON.parse(raw) as Favorite;
|
||||
const keyPart = fullKey.replace(`u:${userName}:fav:`, '');
|
||||
result[keyPart] = fav;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
async deleteFavorite(userName: string, key: string): Promise<void> {
|
||||
await this.client.del(this.favKey(userName, key));
|
||||
}
|
||||
|
||||
// ---------- 用户注册 / 登录 ----------
|
||||
private userPwdKey(user: string) {
|
||||
return `u:${user}:pwd`;
|
||||
}
|
||||
|
||||
async registerUser(userName: string, password: string): Promise<void> {
|
||||
// 简单存储明文密码,生产环境应加密
|
||||
await this.client.set(this.userPwdKey(userName), password);
|
||||
}
|
||||
|
||||
async verifyUser(userName: string, password: string): Promise<boolean> {
|
||||
const stored = await this.client.get(this.userPwdKey(userName));
|
||||
if (stored === null) return false;
|
||||
return stored === password;
|
||||
}
|
||||
|
||||
// ---------- 搜索历史 ----------
|
||||
private shKey = 'moontv:search_history';
|
||||
|
||||
async getSearchHistory(): Promise<string[]> {
|
||||
return (await this.client.lRange(this.shKey, 0, -1)) as string[];
|
||||
}
|
||||
|
||||
async addSearchHistory(keyword: string): Promise<void> {
|
||||
// 先去重
|
||||
await this.client.lRem(this.shKey, 0, keyword);
|
||||
// 插入到最前
|
||||
await this.client.lPush(this.shKey, keyword);
|
||||
// 限制最大长度
|
||||
await this.client.lTrim(this.shKey, 0, SEARCH_HISTORY_LIMIT - 1);
|
||||
}
|
||||
|
||||
async deleteSearchHistory(keyword?: string): Promise<void> {
|
||||
if (keyword) {
|
||||
await this.client.lRem(this.shKey, 0, keyword);
|
||||
} else {
|
||||
await this.client.del(this.shKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建存储实例
|
||||
function createStorage(): IStorage {
|
||||
switch (STORAGE_TYPE) {
|
||||
case 'database':
|
||||
return new DatabaseStorage();
|
||||
case 'redis':
|
||||
return new RedisStorage();
|
||||
case 'localstorage':
|
||||
default:
|
||||
// 默认返回内存实现,保证本地开发可用
|
||||
return null as unknown as IStorage;
|
||||
}
|
||||
}
|
||||
@@ -123,80 +232,152 @@ export class DbManager {
|
||||
}
|
||||
|
||||
// 播放记录相关方法
|
||||
async getPlayRecord(source: string, id: string): Promise<PlayRecord | null> {
|
||||
async getPlayRecord(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<PlayRecord | null> {
|
||||
const key = generateStorageKey(source, id);
|
||||
return this.storage.getPlayRecord(key);
|
||||
return this.storage.getPlayRecord(userName, key);
|
||||
}
|
||||
|
||||
async savePlayRecord(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string,
|
||||
record: Omit<PlayRecord, 'user_id'>
|
||||
): Promise<void> {
|
||||
const key = generateStorageKey(source, id);
|
||||
const fullRecord: PlayRecord = { ...record, user_id: 0 };
|
||||
await this.storage.setPlayRecord(key, fullRecord);
|
||||
await this.storage.setPlayRecord(userName, key, fullRecord);
|
||||
}
|
||||
|
||||
async getAllPlayRecords(): Promise<{ [key: string]: PlayRecord }> {
|
||||
return this.storage.getAllPlayRecords();
|
||||
async getAllPlayRecords(userName: string): Promise<{
|
||||
[key: string]: PlayRecord;
|
||||
}> {
|
||||
return this.storage.getAllPlayRecords(userName);
|
||||
}
|
||||
|
||||
async deletePlayRecord(source: string, id: string): Promise<void> {
|
||||
async deletePlayRecord(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const key = generateStorageKey(source, id);
|
||||
await this.storage.deletePlayRecord(key);
|
||||
await this.storage.deletePlayRecord(userName, key);
|
||||
}
|
||||
|
||||
// 收藏相关方法
|
||||
async getFavorite(source: string, id: string): Promise<Favorite | null> {
|
||||
async getFavorite(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<Favorite | null> {
|
||||
const key = generateStorageKey(source, id);
|
||||
return this.storage.getFavorite(key);
|
||||
return this.storage.getFavorite(userName, key);
|
||||
}
|
||||
|
||||
async saveFavorite(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string,
|
||||
favorite: Omit<Favorite, 'user_id'>
|
||||
): Promise<void> {
|
||||
const key = generateStorageKey(source, id);
|
||||
const fullFavorite: Favorite = { ...favorite, user_id: 0 };
|
||||
await this.storage.setFavorite(key, fullFavorite);
|
||||
await this.storage.setFavorite(userName, key, fullFavorite);
|
||||
}
|
||||
|
||||
async getAllFavorites(): Promise<{ [key: string]: Favorite }> {
|
||||
return this.storage.getAllFavorites();
|
||||
async getAllFavorites(
|
||||
userName: string
|
||||
): Promise<{ [key: string]: Favorite }> {
|
||||
return this.storage.getAllFavorites(userName);
|
||||
}
|
||||
|
||||
async deleteFavorite(source: string, id: string): Promise<void> {
|
||||
async deleteFavorite(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const key = generateStorageKey(source, id);
|
||||
await this.storage.deleteFavorite(key);
|
||||
await this.storage.deleteFavorite(userName, key);
|
||||
}
|
||||
|
||||
async isFavorited(source: string, id: string): Promise<boolean> {
|
||||
const favorite = await this.getFavorite(source, id);
|
||||
async isFavorited(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string
|
||||
): Promise<boolean> {
|
||||
const favorite = await this.getFavorite(userName, source, id);
|
||||
return favorite !== null;
|
||||
}
|
||||
|
||||
async toggleFavorite(
|
||||
userName: string,
|
||||
source: string,
|
||||
id: string,
|
||||
favoriteData?: Omit<Favorite, 'user_id'>
|
||||
): Promise<boolean> {
|
||||
const isFav = await this.isFavorited(source, id);
|
||||
const isFav = await this.isFavorited(userName, source, id);
|
||||
|
||||
if (isFav) {
|
||||
await this.deleteFavorite(source, id);
|
||||
await this.deleteFavorite(userName, source, id);
|
||||
return false;
|
||||
} else {
|
||||
if (favoriteData) {
|
||||
await this.saveFavorite(source, id, favoriteData);
|
||||
return true;
|
||||
} else {
|
||||
throw new Error('Favorite data is required when adding to favorites');
|
||||
}
|
||||
}
|
||||
|
||||
if (favoriteData) {
|
||||
await this.saveFavorite(userName, source, id, favoriteData);
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error('Favorite data is required when adding to favorites');
|
||||
}
|
||||
|
||||
// ---------- 用户相关 ----------
|
||||
async registerUser(userName: string, password: string): Promise<void> {
|
||||
await this.storage.registerUser(userName, password);
|
||||
}
|
||||
|
||||
async verifyUser(userName: string, password: string): Promise<boolean> {
|
||||
return this.storage.verifyUser(userName, password);
|
||||
}
|
||||
|
||||
// ---------- 搜索历史 ----------
|
||||
async getSearchHistory(): Promise<string[]> {
|
||||
return this.storage.getSearchHistory();
|
||||
}
|
||||
|
||||
async addSearchHistory(keyword: string): Promise<void> {
|
||||
await this.storage.addSearchHistory(keyword);
|
||||
}
|
||||
|
||||
async deleteSearchHistory(keyword?: string): Promise<void> {
|
||||
await this.storage.deleteSearchHistory(keyword);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出默认实例
|
||||
export const db = new DbManager();
|
||||
|
||||
// 单例 Redis 客户端
|
||||
function getRedisClient(): RedisClientType {
|
||||
const globalKey = Symbol.for('__MOONTV_REDIS_CLIENT__');
|
||||
let client: RedisClientType | undefined = (global as any)[globalKey];
|
||||
|
||||
if (!client) {
|
||||
const url = process.env.REDIS_URL;
|
||||
if (!url) {
|
||||
throw new Error('REDIS_URL env variable not set');
|
||||
}
|
||||
client = createClient({ url });
|
||||
|
||||
// 提前连接,连接失败抛出错误便于定位
|
||||
client.connect().catch((err: unknown) => {
|
||||
console.error('Redis connection error:', err);
|
||||
});
|
||||
|
||||
(global as any)[globalKey] = client;
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user