feat: init redis storage

This commit is contained in:
shinya
2025-07-02 13:13:22 +08:00
parent 87b65fc7b4
commit 7b3afce1a1
11 changed files with 815 additions and 116 deletions

View 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 }
);
}
}

View File

@@ -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 });
}
}

View File

@@ -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 }
);
}
}

View 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 }
);
}
}