fix: search history isolation

This commit is contained in:
shinya
2025-07-03 22:05:12 +08:00
parent 712b61e1df
commit ac39b76ffc
3 changed files with 76 additions and 34 deletions

View File

@@ -10,12 +10,22 @@ export const runtime = 'edge';
const HISTORY_LIMIT = 20; const HISTORY_LIMIT = 20;
/** /**
* GET /api/searchhistory * GET /api/searchhistory?user=<username>
* 返回 string[] * 返回 string[]
*/ */
export async function GET() { export async function GET(request: NextRequest) {
try { try {
const history = await db.getSearchHistory(); const { searchParams } = new URL(request.url);
const user = searchParams.get('user')?.trim();
if (!user) {
return NextResponse.json(
{ error: 'User parameter is required' },
{ status: 400 }
);
}
const history = await db.getSearchHistory(user);
return NextResponse.json(history, { status: 200 }); return NextResponse.json(history, { status: 200 });
} catch (err) { } catch (err) {
console.error('获取搜索历史失败', err); console.error('获取搜索历史失败', err);
@@ -28,12 +38,14 @@ export async function GET() {
/** /**
* POST /api/searchhistory * POST /api/searchhistory
* body: { keyword: string } * body: { keyword: string, user: string }
*/ */
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const body = await request.json(); const body = await request.json();
const keyword: string = body.keyword?.trim(); const keyword: string = body.keyword?.trim();
const user: string = body.user?.trim();
if (!keyword) { if (!keyword) {
return NextResponse.json( return NextResponse.json(
{ error: 'Keyword is required' }, { error: 'Keyword is required' },
@@ -41,10 +53,17 @@ export async function POST(request: NextRequest) {
); );
} }
await db.addSearchHistory(keyword); if (!user) {
return NextResponse.json(
{ error: 'User parameter is required' },
{ status: 400 }
);
}
await db.addSearchHistory(user, keyword);
// 再次获取最新列表,确保客户端与服务端同步 // 再次获取最新列表,确保客户端与服务端同步
const history = await db.getSearchHistory(); const history = await db.getSearchHistory(user);
return NextResponse.json(history.slice(0, HISTORY_LIMIT), { status: 200 }); return NextResponse.json(history.slice(0, HISTORY_LIMIT), { status: 200 });
} catch (err) { } catch (err) {
console.error('添加搜索历史失败', err); console.error('添加搜索历史失败', err);
@@ -56,7 +75,7 @@ export async function POST(request: NextRequest) {
} }
/** /**
* DELETE /api/searchhistory * DELETE /api/searchhistory?user=<username>&keyword=<kw>
* *
* 1. 不带 keyword -> 清空全部搜索历史 * 1. 不带 keyword -> 清空全部搜索历史
* 2. 带 keyword=<kw> -> 删除单条关键字 * 2. 带 keyword=<kw> -> 删除单条关键字
@@ -64,9 +83,17 @@ export async function POST(request: NextRequest) {
export async function DELETE(request: NextRequest) { export async function DELETE(request: NextRequest) {
try { try {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const user = searchParams.get('user')?.trim();
const kw = searchParams.get('keyword')?.trim(); const kw = searchParams.get('keyword')?.trim();
await db.deleteSearchHistory(kw || undefined); if (!user) {
return NextResponse.json(
{ error: 'User parameter is required' },
{ status: 400 }
);
}
await db.deleteSearchHistory(user, kw || undefined);
return NextResponse.json({ success: true }, { status: 200 }); return NextResponse.json({ success: true }, { status: 200 });
} catch (err) { } catch (err) {

View File

@@ -206,7 +206,10 @@ export async function getSearchHistory(): Promise<string[]> {
// 如果配置为使用数据库,则从后端 API 获取 // 如果配置为使用数据库,则从后端 API 获取
if (STORAGE_TYPE !== 'localstorage') { if (STORAGE_TYPE !== 'localstorage') {
try { try {
return fetchFromApi<string[]>('/api/searchhistory'); const user = getUsername();
return fetchFromApi<string[]>(
`/api/searchhistory?user=${encodeURIComponent(user ?? '')}`
);
} catch (err) { } catch (err) {
console.error('获取搜索历史失败:', err); console.error('获取搜索历史失败:', err);
return []; return [];
@@ -240,12 +243,13 @@ export async function addSearchHistory(keyword: string): Promise<void> {
// 数据库模式 // 数据库模式
if (STORAGE_TYPE !== 'localstorage') { if (STORAGE_TYPE !== 'localstorage') {
try { try {
const user = getUsername();
await fetch('/api/searchhistory', { await fetch('/api/searchhistory', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ keyword: trimmed }), body: JSON.stringify({ keyword: trimmed, user: user ?? '' }),
}); });
} catch (err) { } catch (err) {
console.error('保存搜索历史失败:', err); console.error('保存搜索历史失败:', err);
@@ -276,7 +280,8 @@ export async function clearSearchHistory(): Promise<void> {
// 数据库模式 // 数据库模式
if (STORAGE_TYPE !== 'localstorage') { if (STORAGE_TYPE !== 'localstorage') {
try { try {
await fetch('/api/searchhistory', { const user = getUsername();
await fetch(`/api/searchhistory?user=${encodeURIComponent(user ?? '')}`, {
method: 'DELETE', method: 'DELETE',
}); });
} catch (err) { } catch (err) {
@@ -300,9 +305,15 @@ export async function deleteSearchHistory(keyword: string): Promise<void> {
// 数据库模式 // 数据库模式
if (STORAGE_TYPE !== 'localstorage') { if (STORAGE_TYPE !== 'localstorage') {
try { try {
await fetch(`/api/searchhistory?keyword=${encodeURIComponent(trimmed)}`, { const user = getUsername();
method: 'DELETE', await fetch(
}); `/api/searchhistory?user=${encodeURIComponent(
user ?? ''
)}&keyword=${encodeURIComponent(trimmed)}`,
{
method: 'DELETE',
}
);
} catch (err) { } catch (err) {
console.error('删除搜索历史失败:', err); console.error('删除搜索历史失败:', err);
} }

View File

@@ -58,9 +58,9 @@ export interface IStorage {
checkUserExist(userName: string): Promise<boolean>; checkUserExist(userName: string): Promise<boolean>;
// 搜索历史相关 // 搜索历史相关
getSearchHistory(): Promise<string[]>; getSearchHistory(userName: string): Promise<string[]>;
addSearchHistory(keyword: string): Promise<void>; addSearchHistory(userName: string, keyword: string): Promise<void>;
deleteSearchHistory(keyword?: string): Promise<void>; deleteSearchHistory(userName: string, keyword?: string): Promise<void>;
// 用户列表 // 用户列表
getAllUsers(): Promise<string[]>; getAllUsers(): Promise<string[]>;
@@ -184,26 +184,30 @@ class RedisStorage implements IStorage {
} }
// ---------- 搜索历史 ---------- // ---------- 搜索历史 ----------
private shKey = 'moontv:search_history'; private shKey(user: string) {
return `u:${user}:sh`; // u:username:sh
async getSearchHistory(): Promise<string[]> {
return (await this.client.lRange(this.shKey, 0, -1)) as string[];
} }
async addSearchHistory(keyword: string): Promise<void> { async getSearchHistory(userName: string): Promise<string[]> {
return (await this.client.lRange(this.shKey(userName), 0, -1)) as string[];
}
async addSearchHistory(userName: string, keyword: string): Promise<void> {
const key = this.shKey(userName);
// 先去重 // 先去重
await this.client.lRem(this.shKey, 0, keyword); await this.client.lRem(key, 0, keyword);
// 插入到最前 // 插入到最前
await this.client.lPush(this.shKey, keyword); await this.client.lPush(key, keyword);
// 限制最大长度 // 限制最大长度
await this.client.lTrim(this.shKey, 0, SEARCH_HISTORY_LIMIT - 1); await this.client.lTrim(key, 0, SEARCH_HISTORY_LIMIT - 1);
} }
async deleteSearchHistory(keyword?: string): Promise<void> { async deleteSearchHistory(userName: string, keyword?: string): Promise<void> {
const key = this.shKey(userName);
if (keyword) { if (keyword) {
await this.client.lRem(this.shKey, 0, keyword); await this.client.lRem(key, 0, keyword);
} else { } else {
await this.client.del(this.shKey); await this.client.del(key);
} }
} }
@@ -371,16 +375,16 @@ export class DbManager {
} }
// ---------- 搜索历史 ---------- // ---------- 搜索历史 ----------
async getSearchHistory(): Promise<string[]> { async getSearchHistory(userName: string): Promise<string[]> {
return this.storage.getSearchHistory(); return this.storage.getSearchHistory(userName);
} }
async addSearchHistory(keyword: string): Promise<void> { async addSearchHistory(userName: string, keyword: string): Promise<void> {
await this.storage.addSearchHistory(keyword); await this.storage.addSearchHistory(userName, keyword);
} }
async deleteSearchHistory(keyword?: string): Promise<void> { async deleteSearchHistory(userName: string, keyword?: string): Promise<void> {
await this.storage.deleteSearchHistory(keyword); await this.storage.deleteSearchHistory(userName, keyword);
} }
// 获取全部用户名 // 获取全部用户名