Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | 4x 4x 4x 4x 4x 1x 1x 3x 3x 3x 3x 3x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 3x 3x 4x | /**
* Catfood Realtime Hook - 猫粮数据实时同步
*
* 功能:
* - 监听 catfoods 表的 UPDATE 事件(评分、点赞等统计数据变化)
* - 监听 catfood_ratings 表的 INSERT/UPDATE/DELETE 事件
* - 监听 catfood_likes 表的 INSERT/DELETE 事件
* - 自动更新 catFoodStore 中的数据
* - 支持按需订阅特定猫粮
*/
import { useEffect, useRef } from 'react';
import { RealtimeChannel } from '@supabase/supabase-js';
import { supabase } from '@/src/lib/supabase/client';
import { useCatFoodStore } from '@/src/store/catFoodStore';
import { logger } from '@/src/utils/logger';
import type { CatFood } from '@/src/types/catFood';
interface UseCatfoodRealtimeOptions {
/**
* 是否启用实时订阅(默认 true)
*/
enabled?: boolean;
/**
* 订阅特定的猫粮 ID(如果不指定则订阅所有)
*/
catfoodId?: number;
/**
* 数据变化回调
*/
onUpdate?: (catfood: CatFood) => void;
}
/**
* Catfood 实时同步 Hook
*
* @example
* ```tsx
* // 在排行榜页面订阅所有猫粮变化
* useCatfoodRealtime({ enabled: true });
*
* // 在详情页订阅特定猫粮
* useCatfoodRealtime({
* enabled: true,
* catfoodId: 123,
* onUpdate: (catfood) => console.log('Updated:', catfood)
* });
* ```
*/
export function useCatfoodRealtime(options: UseCatfoodRealtimeOptions = {}) {
const { enabled = true, catfoodId, onUpdate } = options;
const channelRef = useRef<RealtimeChannel | null>(null);
const updateCatFood = useCatFoodStore((state) => state.updateCatFood);
useEffect(() => {
if (!enabled) {
logger.debug('Realtime 订阅未启用');
return;
}
// 创建唯一的频道名称
const channelName = catfoodId ? `catfood-realtime-${catfoodId}` : 'catfood-realtime-all';
logger.info('🔌 启动 Catfood Realtime 订阅', { channelName, catfoodId });
// 创建 Realtime 频道
const channel = supabase.channel(channelName);
// 订阅 catfoods 表的更新(评分、点赞统计等)
const catfoodsFilter = catfoodId ? `id=eq.${catfoodId}` : undefined;
channel.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'catfoods',
filter: catfoodsFilter,
},
(payload) => {
logger.info('📊 Catfood 更新', payload);
const updatedCatfood = payload.new as any;
// 更新 store
updateCatFood(updatedCatfood.id, {
score: updatedCatfood.score,
countNum: updatedCatfood.count_num,
});
// 触发回调
Eif (onUpdate) {
onUpdate(updatedCatfood);
}
}
);
// 📝 注意:不需要订阅 catfood_ratings 和 catfood_likes 表
// 因为数据库触发器会自动更新 catfoods 表,我们只需监听 catfoods 的 UPDATE 事件
// 这样可以避免重复刷新和页面重新加载
// 订阅频道
channel.subscribe((status) => {
if (status === 'SUBSCRIBED') {
logger.info('✅ Realtime 订阅成功', { channelName });
} else if (status === 'CHANNEL_ERROR') {
// WebSocket 连接错误是正常的,通常是网络波动或热重载导致
// Supabase 会自动重连,不需要特别处理
logger.warn('⚠️ Realtime 连接中断,正在重连...', { channelName });
} else if (status === 'TIMED_OUT') {
logger.error('❌ Realtime 订阅超时', new Error(status));
} else if (status === 'CLOSED') {
logger.info('🔌 Realtime 连接已关闭', { channelName });
}
});
channelRef.current = channel;
// 清理函数
return () => {
logger.info('🔌 关闭 Catfood Realtime 订阅', { channelName });
Eif (channelRef.current) {
supabase.removeChannel(channelRef.current);
channelRef.current = null;
}
};
}, [enabled, catfoodId, updateCatFood, onUpdate]);
return {
channel: channelRef.current,
};
}
/**
* 监听评论数量变化(用于详情页)
*/
export function useCommentsRealtime(options: {
targetType: 'catfood' | 'post' | 'report';
targetId: number;
enabled?: boolean;
onUpdate?: () => void;
}) {
const { targetType, targetId, enabled = true, onUpdate } = options;
const channelRef = useRef<RealtimeChannel | null>(null);
useEffect(() => {
if (!enabled) return;
const channelName = `comments-${targetType}-${targetId}`;
logger.info('🔌 启动 Comments Realtime 订阅', { channelName });
const channel = supabase.channel(channelName);
channel.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'comments',
filter: `target_type=eq.${targetType},target_id=eq.${targetId}`,
},
(payload) => {
logger.info('💬 评论变化', payload);
onUpdate?.();
}
);
channel.subscribe((status) => {
if (status === 'SUBSCRIBED') {
logger.info('✅ Comments Realtime 订阅成功');
}
});
channelRef.current = channel;
return () => {
logger.info('🔌 关闭 Comments Realtime 订阅');
if (channelRef.current) {
supabase.removeChannel(channelRef.current);
channelRef.current = null;
}
};
}, [enabled, targetType, targetId, onUpdate]);
return { channel: channelRef.current };
}
|