增加浏览器离线通知功能

This commit is contained in:
mtvpls
2026-06-02 20:50:33 +08:00
parent 16e48bef8b
commit d600317ade
20 changed files with 1739 additions and 91 deletions
+133 -1
View File
@@ -14,6 +14,7 @@ import {
DanmakuFilterConfig,
Notification,
MovieRequest,
PushSubscriptionRecord,
} from './types';
import { AdminConfig } from './admin.types';
import { MangaReadRecord, MangaShelfItem } from './manga.types';
@@ -25,6 +26,7 @@ import {
MusicV2PlaylistRecord,
} from './music-v2';
import { userInfoCache } from './user-cache';
import { dispatchWebPushNotification } from './web-push';
/**
* Cloudflare D1 存储实现
@@ -81,6 +83,7 @@ export class D1Storage implements IStorage {
}
}
// ==================== 播放记录 ====================
async getPlayRecord(
@@ -1911,6 +1914,133 @@ export class D1Storage implements IStorage {
}
}
async upsertPushSubscription(
userName: string,
subscription: PushSubscriptionRecord
): Promise<void> {
try {
const result = await this.db
.prepare(`
INSERT INTO notification_push_subscriptions (
id, username, token_id, endpoint, p256dh, auth, user_agent, enabled,
created_at, updated_at, last_success_at, last_failure_at, failure_count
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, 0)
ON CONFLICT(endpoint) DO UPDATE SET
username = excluded.username,
token_id = excluded.token_id,
p256dh = excluded.p256dh,
auth = excluded.auth,
user_agent = excluded.user_agent,
enabled = 1,
updated_at = excluded.updated_at
`)
.bind(
subscription.id,
userName,
subscription.tokenId || null,
subscription.endpoint,
subscription.p256dh,
subscription.auth,
subscription.userAgent || null,
subscription.enabled ? 1 : 0,
subscription.createdAt,
subscription.updatedAt
)
.run();
if (!result.success) {
throw new Error(result.error || '保存浏览器通知订阅失败');
}
} catch (err) {
console.error('D1Storage.upsertPushSubscription error:', err);
throw err;
}
}
async getEnabledPushSubscriptions(userName: string): Promise<PushSubscriptionRecord[]> {
try {
const results = await this.db
.prepare('SELECT * FROM notification_push_subscriptions WHERE username = ? AND enabled = 1')
.bind(userName)
.all();
return (results.results || []).map((row: any) => ({
id: row.id as string,
username: row.username as string,
tokenId: (row.token_id as string | null) || null,
endpoint: row.endpoint as string,
p256dh: row.p256dh as string,
auth: row.auth as string,
userAgent: (row.user_agent as string | null) || null,
enabled: row.enabled === 1,
createdAt: Number(row.created_at),
updatedAt: Number(row.updated_at),
lastSuccessAt: row.last_success_at ? Number(row.last_success_at) : null,
lastFailureAt: row.last_failure_at ? Number(row.last_failure_at) : null,
failureCount: Number(row.failure_count || 0),
}));
} catch (err) {
console.error('D1Storage.getEnabledPushSubscriptions error:', err);
return [];
}
}
async deletePushSubscriptionByEndpoint(userName: string, endpoint: string): Promise<void> {
try {
await this.db
.prepare('DELETE FROM notification_push_subscriptions WHERE username = ? AND endpoint = ?')
.bind(userName, endpoint)
.run();
} catch (err) {
console.error('D1Storage.deletePushSubscriptionByEndpoint error:', err);
}
}
async deletePushSubscriptionsByTokenId(userName: string, tokenId: string): Promise<void> {
try {
await this.db
.prepare('DELETE FROM notification_push_subscriptions WHERE username = ? AND token_id = ?')
.bind(userName, tokenId)
.run();
} catch (err) {
console.error('D1Storage.deletePushSubscriptionsByTokenId error:', err);
}
}
async deleteAllPushSubscriptions(userName: string): Promise<void> {
try {
await this.db
.prepare('DELETE FROM notification_push_subscriptions WHERE username = ?')
.bind(userName)
.run();
} catch (err) {
console.error('D1Storage.deleteAllPushSubscriptions error:', err);
}
}
async updatePushSubscriptionDeliveryStats(
userName: string,
endpoint: string,
success: boolean
): Promise<void> {
try {
const now = Date.now();
if (success) {
await this.db
.prepare('UPDATE notification_push_subscriptions SET last_success_at = ?, failure_count = 0, updated_at = ? WHERE username = ? AND endpoint = ?')
.bind(now, now, userName, endpoint)
.run();
} else {
await this.db
.prepare('UPDATE notification_push_subscriptions SET last_failure_at = ?, failure_count = failure_count + 1, updated_at = ? WHERE username = ? AND endpoint = ?')
.bind(now, now, userName, endpoint)
.run();
}
} catch (err) {
console.error('D1Storage.updatePushSubscriptionDeliveryStats error:', err);
}
}
// ==================== TVBox订阅token ====================
async getTvboxSubscribeToken?(userName: string): Promise<string | null> {
@@ -2952,6 +3082,8 @@ export class D1Storage implements IStorage {
notification.metadata ? JSON.stringify(notification.metadata) : null
)
.run();
await dispatchWebPushNotification(this, userName, notification);
} catch (err) {
console.error('D1Storage.addNotification error:', err);
throw err;
@@ -3059,7 +3191,7 @@ export class D1Storage implements IStorage {
requested_by, request_count, status, created_at, updated_at,
fulfilled_at, fulfilled_source, fulfilled_id
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
)
.bind(