520 lines
12 KiB
JavaScript
520 lines
12 KiB
JavaScript
-- =============================================================================
|
||
-- Supabase 消息系统客户端配置和使用示例
|
||
-- JavaScript/TypeScript 客户端代码示例
|
||
-- =============================================================================
|
||
|
||
/*
|
||
1. Supabase 客户端初始化
|
||
*/
|
||
|
||
import { createClient } from '@supabase/supabase-js'
|
||
|
||
const supabaseUrl = 'your-supabase-url'
|
||
const supabaseAnonKey = 'your-supabase-anon-key'
|
||
|
||
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||
auth: {
|
||
autoRefreshToken: true,
|
||
persistSession: true,
|
||
detectSessionInUrl: true
|
||
},
|
||
realtime: {
|
||
params: {
|
||
eventsPerSecond: 10
|
||
}
|
||
}
|
||
})
|
||
|
||
/*
|
||
2. 用户认证和角色设置
|
||
*/
|
||
|
||
// 登录函数 - 需要在JWT中包含用户角色
|
||
export async function signInUser(email: string, password: string) {
|
||
const { data, error } = await supabase.auth.signInWithPassword({
|
||
email,
|
||
password
|
||
})
|
||
|
||
if (error) throw error
|
||
|
||
// 获取用户角色信息(从数据库或其他来源)
|
||
const userRole = await getUserRole(data.user.id)
|
||
|
||
// 更新用户元数据包含角色信息
|
||
await supabase.auth.updateUser({
|
||
data: {
|
||
user_role: userRole,
|
||
role: userRole
|
||
}
|
||
})
|
||
|
||
return data
|
||
}
|
||
|
||
// 获取用户角色
|
||
async function getUserRole(userId: string): Promise<string> {
|
||
// 从用户表获取角色信息
|
||
const { data, error } = await supabase
|
||
.from('ak_users')
|
||
.select('role, user_type')
|
||
.eq('id', userId)
|
||
.single()
|
||
|
||
if (error) throw error
|
||
|
||
return data.role || data.user_type || 'student'
|
||
}
|
||
|
||
/*
|
||
3. 消息查询 - 利用RLS自动过滤
|
||
*/
|
||
|
||
// 获取用户消息列表(RLS会自动过滤权限)
|
||
export async function getUserMessages(userId: string, limit = 20) {
|
||
const { data, error } = await supabase
|
||
.from('ak_messages')
|
||
.select(`
|
||
id,
|
||
title,
|
||
content,
|
||
created_at,
|
||
is_urgent,
|
||
sender_name,
|
||
ak_message_types (
|
||
name,
|
||
icon,
|
||
color
|
||
),
|
||
ak_message_recipients!inner (
|
||
status,
|
||
read_at,
|
||
is_starred,
|
||
is_archived
|
||
)
|
||
`)
|
||
.eq('ak_message_recipients.recipient_id', userId)
|
||
.eq('ak_message_recipients.recipient_type', 'user')
|
||
.eq('ak_message_recipients.is_deleted', false)
|
||
.order('created_at', { ascending: false })
|
||
.limit(limit)
|
||
|
||
if (error) throw error
|
||
return data
|
||
}
|
||
|
||
// 获取未读消息数量
|
||
export async function getUnreadCount(userId: string) {
|
||
const { data, error } = await supabase
|
||
.from('ak_message_recipients')
|
||
.select('message_id', { count: 'exact' })
|
||
.eq('recipient_id', userId)
|
||
.eq('recipient_type', 'user')
|
||
.neq('status', 'read')
|
||
.eq('is_deleted', false)
|
||
|
||
if (error) throw error
|
||
return data?.length || 0
|
||
}
|
||
|
||
// 获取消息类型未读统计
|
||
export async function getUnreadCountByType(userId: string) {
|
||
const { data, error } = await supabase
|
||
.rpc('get_unread_message_count', { p_user_id: userId })
|
||
|
||
if (error) throw error
|
||
return data
|
||
}
|
||
|
||
/*
|
||
4. 发送消息 - 使用系统函数
|
||
*/
|
||
|
||
// 发送消息(教师发送给学生)
|
||
export async function sendMessage(messageData: {
|
||
messageType: string
|
||
receiverType: string
|
||
receiverId: string
|
||
title: string
|
||
content: string
|
||
priority?: number
|
||
metadata?: any
|
||
}) {
|
||
const { data, error } = await supabase
|
||
.rpc('send_message', {
|
||
p_message_type_code: messageData.messageType,
|
||
p_sender_type: 'user',
|
||
p_sender_id: (await supabase.auth.getUser()).data.user?.id,
|
||
p_receiver_type: messageData.receiverType,
|
||
p_receiver_id: messageData.receiverId,
|
||
p_title: messageData.title,
|
||
p_content: messageData.content,
|
||
p_priority: messageData.priority || 50,
|
||
p_metadata: messageData.metadata || null
|
||
})
|
||
|
||
if (error) throw error
|
||
return data
|
||
}
|
||
|
||
// 教师发送作业通知
|
||
export async function sendAssignmentNotification(
|
||
studentId: string,
|
||
assignmentTitle: string,
|
||
dueDate: string,
|
||
description: string
|
||
) {
|
||
return await sendMessage({
|
||
messageType: 'assignment',
|
||
receiverType: 'user',
|
||
receiverId: studentId,
|
||
title: `新作业:${assignmentTitle}`,
|
||
content: `${description}\n\n截止时间:${dueDate}`,
|
||
priority: 85,
|
||
metadata: {
|
||
assignment_title: assignmentTitle,
|
||
due_date: dueDate,
|
||
type: 'assignment_notification'
|
||
}
|
||
})
|
||
}
|
||
|
||
/*
|
||
5. 消息操作 - 标记已读、标星等
|
||
*/
|
||
|
||
// 标记消息为已读
|
||
export async function markMessageAsRead(messageId: string, userId: string) {
|
||
const { data, error } = await supabase
|
||
.rpc('mark_message_read', {
|
||
p_message_id: messageId,
|
||
p_user_id: userId
|
||
})
|
||
|
||
if (error) throw error
|
||
return data
|
||
}
|
||
|
||
// 标记消息为星标
|
||
export async function toggleMessageStar(messageId: string, userId: string, isStarred: boolean) {
|
||
const { data, error } = await supabase
|
||
.from('ak_message_recipients')
|
||
.update({ is_starred: isStarred })
|
||
.eq('message_id', messageId)
|
||
.eq('recipient_id', userId)
|
||
.eq('recipient_type', 'user')
|
||
|
||
if (error) throw error
|
||
return data
|
||
}
|
||
|
||
// 归档消息
|
||
export async function archiveMessage(messageId: string, userId: string) {
|
||
const { data, error } = await supabase
|
||
.from('ak_message_recipients')
|
||
.update({ is_archived: true })
|
||
.eq('message_id', messageId)
|
||
.eq('recipient_id', userId)
|
||
.eq('recipient_type', 'user')
|
||
|
||
if (error) throw error
|
||
return data
|
||
}
|
||
|
||
/*
|
||
6. 群组消息功能
|
||
*/
|
||
|
||
// 获取用户群组列表
|
||
export async function getUserGroups(userId: string) {
|
||
const { data, error } = await supabase
|
||
.from('ak_message_groups')
|
||
.select(`
|
||
id,
|
||
name,
|
||
description,
|
||
group_type,
|
||
is_public,
|
||
created_at,
|
||
ak_message_group_members!inner (
|
||
role,
|
||
status,
|
||
joined_at
|
||
)
|
||
`)
|
||
.eq('ak_message_group_members.user_id', userId)
|
||
.eq('ak_message_group_members.status', 'active')
|
||
.eq('is_active', true)
|
||
|
||
if (error) throw error
|
||
return data
|
||
}
|
||
|
||
// 发送群组消息
|
||
export async function sendGroupMessage(
|
||
groupId: string,
|
||
title: string,
|
||
content: string,
|
||
metadata?: any
|
||
) {
|
||
return await sendMessage({
|
||
messageType: 'chat',
|
||
receiverType: 'group',
|
||
receiverId: groupId,
|
||
title: title,
|
||
content: content,
|
||
priority: 60,
|
||
metadata: {
|
||
...metadata,
|
||
message_type: 'group_chat'
|
||
}
|
||
})
|
||
}
|
||
|
||
/*
|
||
7. 实时订阅 - 自动受RLS策略保护
|
||
*/
|
||
|
||
// 订阅用户消息更新
|
||
export function subscribeToUserMessages(userId: string, callback: (payload: any) => void) {
|
||
return supabase
|
||
.channel('user-messages')
|
||
.on(
|
||
'postgres_changes',
|
||
{
|
||
event: '*',
|
||
schema: 'public',
|
||
table: 'ak_message_recipients',
|
||
filter: `recipient_id=eq.${userId}`
|
||
},
|
||
callback
|
||
)
|
||
.subscribe()
|
||
}
|
||
|
||
// 订阅群组消息更新
|
||
export function subscribeToGroupMessages(groupId: string, callback: (payload: any) => void) {
|
||
return supabase
|
||
.channel(`group-${groupId}`)
|
||
.on(
|
||
'postgres_changes',
|
||
{
|
||
event: 'INSERT',
|
||
schema: 'public',
|
||
table: 'ak_messages',
|
||
filter: `receiver_id=eq.${groupId}`
|
||
},
|
||
callback
|
||
)
|
||
.subscribe()
|
||
}
|
||
|
||
// 订阅新消息通知
|
||
export function subscribeToNewMessages(userId: string, callback: (payload: any) => void) {
|
||
return supabase
|
||
.channel('new-messages')
|
||
.on(
|
||
'postgres_changes',
|
||
{
|
||
event: 'INSERT',
|
||
schema: 'public',
|
||
table: 'ak_message_recipients',
|
||
filter: `recipient_id=eq.${userId}`
|
||
},
|
||
async (payload) => {
|
||
// 获取完整消息信息
|
||
const { data: message } = await supabase
|
||
.from('ak_messages')
|
||
.select(`
|
||
*,
|
||
ak_message_types (name, icon, color)
|
||
`)
|
||
.eq('id', payload.new.message_id)
|
||
.single()
|
||
|
||
callback({ ...payload, message })
|
||
}
|
||
)
|
||
.subscribe()
|
||
}
|
||
|
||
/*
|
||
8. 权限检查辅助函数
|
||
*/
|
||
|
||
// 检查用户是否可以访问消息
|
||
export async function canAccessMessage(messageId: string, userId: string): Promise<boolean> {
|
||
const { data, error } = await supabase
|
||
.rpc('can_access_message', {
|
||
message_uuid: messageId,
|
||
user_uuid: userId
|
||
})
|
||
|
||
if (error) return false
|
||
return data || false
|
||
}
|
||
|
||
// 检查用户是否为群组成员
|
||
export async function isGroupMember(groupId: string, userId: string): Promise<boolean> {
|
||
const { data, error } = await supabase
|
||
.rpc('is_group_member', {
|
||
group_uuid: groupId,
|
||
user_uuid: userId
|
||
})
|
||
|
||
if (error) return false
|
||
return data || false
|
||
}
|
||
|
||
/*
|
||
9. 教师端特殊功能
|
||
*/
|
||
|
||
// 教师获取学生消息统计
|
||
export async function getStudentMessageStats(teacherId: string, studentId: string) {
|
||
// 首先验证教师权限(可以根据业务逻辑调整)
|
||
const { data: teacherUser } = await supabase.auth.getUser()
|
||
if (!teacherUser?.user || teacherUser.user.id !== teacherId) {
|
||
throw new Error('权限不足:只能查看自己负责的学生数据')
|
||
}
|
||
|
||
// 获取学生消息统计
|
||
const { data, error } = await supabase
|
||
.from('ak_message_recipients')
|
||
.select(`
|
||
status,
|
||
read_at,
|
||
created_at,
|
||
ak_messages (
|
||
title,
|
||
created_at,
|
||
ak_message_types (name)
|
||
)
|
||
`)
|
||
.eq('recipient_id', studentId)
|
||
.order('created_at', { ascending: false })
|
||
.limit(50)
|
||
|
||
if (error) throw error
|
||
return data
|
||
}
|
||
|
||
// 教师发送班级公告
|
||
export async function sendClassAnnouncement(
|
||
classId,
|
||
title,
|
||
content,
|
||
isUrgent = false
|
||
) {
|
||
return await sendMessage({
|
||
messageType: 'announcement',
|
||
receiverType: 'class',
|
||
receiverId: classId,
|
||
title: title,
|
||
content: content,
|
||
priority: isUrgent ? 95 : 80,
|
||
metadata: {
|
||
type: 'class_announcement',
|
||
is_urgent: isUrgent
|
||
}
|
||
})
|
||
}
|
||
|
||
/*
|
||
10. 数据类型定义(JSDoc 风格)
|
||
*/
|
||
|
||
/**
|
||
* @typedef {Object} MessageData
|
||
* @property {string} id
|
||
* @property {string} title
|
||
* @property {string} content
|
||
* @property {string} created_at
|
||
* @property {boolean} is_urgent
|
||
* @property {string} sender_name
|
||
* @property {Object} message_type
|
||
* @property {string} message_type.name
|
||
* @property {string} message_type.icon
|
||
* @property {string} message_type.color
|
||
* @property {Object} recipient_info
|
||
* @property {string} recipient_info.status
|
||
* @property {string|null} recipient_info.read_at
|
||
* @property {boolean} recipient_info.is_starred
|
||
* @property {boolean} recipient_info.is_archived
|
||
*/
|
||
|
||
/**
|
||
* @typedef {Object} GroupData
|
||
* @property {string} id
|
||
* @property {string} name
|
||
* @property {string} description
|
||
* @property {string} group_type
|
||
* @property {boolean} is_public
|
||
* @property {string} member_role
|
||
* @property {string} member_status
|
||
*/
|
||
|
||
// 错误处理包装器
|
||
export function handleSupabaseError(error) {
|
||
console.error('Supabase Error:', error)
|
||
|
||
if (error.code === 'PGRST116') {
|
||
throw new Error('权限不足:您无权执行此操作')
|
||
} else if (error.code === 'PGRST301') {
|
||
throw new Error('数据不存在或无权访问')
|
||
} else {
|
||
throw new Error(error.message || '操作失败')
|
||
}
|
||
}
|
||
|
||
/*
|
||
使用示例:
|
||
|
||
// 教师端登录
|
||
const teacherUser = await signInUser('teacher@example.com', 'password')
|
||
|
||
// 获取教师消息
|
||
const teacherMessages = await getUserMessages('7bf7378e-a027-473e-97ac-3460ed3f170a')
|
||
|
||
// 教师发送作业通知给学生
|
||
await sendAssignmentNotification(
|
||
'eed3824b-bba1-4309-8048-19d17367c084',
|
||
'运动数据分析报告',
|
||
'2024-01-20 23:59',
|
||
'请分析您近一周的运动数据并提交报告'
|
||
)
|
||
|
||
// 学生端登录
|
||
const studentUser = await signInUser('student@example.com', 'password')
|
||
|
||
// 获取学生消息
|
||
const studentMessages = await getUserMessages('eed3824b-bba1-4309-8048-19d17367c084')
|
||
|
||
// 标记消息为已读
|
||
await markMessageAsRead(messageId, 'eed3824b-bba1-4309-8048-19d17367c084')
|
||
|
||
// 订阅实时消息
|
||
const subscription = subscribeToUserMessages(
|
||
'eed3824b-bba1-4309-8048-19d17367c084',
|
||
(payload) => {
|
||
console.log('新消息:', payload)
|
||
// 更新UI显示新消息
|
||
}
|
||
)
|
||
*/
|
||
|
||
export default {
|
||
supabase,
|
||
signInUser,
|
||
getUserMessages,
|
||
getUnreadCount,
|
||
sendMessage,
|
||
markMessageAsRead,
|
||
toggleMessageStar,
|
||
subscribeToUserMessages,
|
||
subscribeToNewMessages,
|
||
sendAssignmentNotification,
|
||
sendClassAnnouncement,
|
||
handleSupabaseError
|
||
}
|