Initial commit of akmon project
This commit is contained in:
519
supabase_message_client.js
Normal file
519
supabase_message_client.js
Normal file
@@ -0,0 +1,519 @@
|
||||
-- =============================================================================
|
||||
-- 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
|
||||
}
|
||||
Reference in New Issue
Block a user