家校沟通互动平台设计与实现
家校沟通是教育信息化的重要组成部分。一个优秀的家校互动平台能够促进教师与家长之间的有效沟通,共同关注学生成长。本文将系统讲解家校沟通平台的设计与实现方案。
平台功能架构
家校沟通平台的核心功能模块:
| 功能模块 | 主要功能 | 使用者 |
|---|---|---|
| 通知公告 | 学校/班级通知、活动公告 | 教师发布,家长查看 |
| 即时消息 | 一对一沟通、群组交流 | 教师、家长双向 |
| 成长档案 | 学生表现记录、成长轨迹 | 教师记录,家长查看 |
| 作业反馈 | 作业完成情况、学习建议 | 教师发布,家长确认 |
| 家长会 | 在线预约、会议记录 | 教师组织,家长参与 |
| 请假管理 | 学生请假申请与审批 | 家长申请,教师审批 |
| 费用缴纳 | 学费、活动费用管理 | 学校发布,家长缴纳 |
数据模型设计
核心实体定义
// 家长账户
interface ParentAccount {
id: string
userId: string // 关联用户系统
phone: string // 手机号
name: string // 家长姓名
relation: ParentRelation // 与学生关系
children: StudentBinding[] // 绑定的学生
notifySettings: NotifySettings // 通知设置
createdAt: Date
}
// 家长与学生的关系
type ParentRelation = 'father' | 'mother' | 'grandfather' | 'grandmother' | 'guardian' | 'other'
// 学生绑定关系
interface StudentBinding {
studentId: string
studentName: string
classId: string
className: string
relation: ParentRelation
bindTime: Date
isPrimary: boolean // 是否为主要联系人
}
// 通知设置
interface NotifySettings {
pushEnabled: boolean // 是否开启推送
smsEnabled: boolean // 是否开启短信
emailEnabled: boolean // 是否开启邮件
quietHours: { // 免打扰时段
enabled: boolean
start: string // 如 "22:00"
end: string // 如 "07:00"
}
categories: { // 分类设置
announcement: boolean // 通知公告
homework: boolean // 作业相关
attendance: boolean // 考勤相关
grades: boolean // 成绩相关
activities: boolean // 活动相关
}
}
消息与通知模型
// 通知公告
interface Announcement {
id: string
title: string
content: string // 富文本内容
type: AnnouncementType
scope: {
type: 'school' | 'grade' | 'class' | 'student'
targetIds: string[]
}
attachments: Attachment[]
requireConfirm: boolean // 是否需要确认已读
confirmDeadline?: Date // 确认截止时间
publisherId: string
publisherName: string
publisherRole: 'admin' | 'teacher'
publishTime: Date
status: 'draft' | 'published' | 'archived'
readStats: {
total: number
read: number
confirmed: number
}
}
type AnnouncementType = 'notice' | 'activity' | 'emergency' | 'policy' | 'reminder'
// 私信消息
interface PrivateMessage {
id: string
conversationId: string
senderId: string
senderName: string
senderRole: 'teacher' | 'parent'
receiverId: string
receiverName: string
content: string
messageType: 'text' | 'image' | 'voice' | 'file'
attachments?: Attachment[]
readAt?: Date
createdAt: Date
}
// 会话
interface Conversation {
id: string
participants: {
id: string
name: string
role: 'teacher' | 'parent'
avatar?: string
}[]
studentId: string // 关联学生
studentName: string
lastMessage: {
content: string
senderId: string
createdAt: Date
}
unreadCount: Record<string, number> // 各参与者未读数
createdAt: Date
updatedAt: Date
}
通知公告系统
通知发布组件
<template>
<div class="announcement-editor">
<Form ref="formRef" :model="formData" :rules="formRules">
<!-- 基础信息 -->
<FormItem label="通知类型" prop="type">
<RadioGroup v-model="formData.type">
<Radio value="notice">普通通知</Radio>
<Radio value="activity">活动公告</Radio>
<Radio value="emergency">紧急通知</Radio>
<Radio value="reminder">温馨提示</Radio>
</RadioGroup>
</FormItem>
<FormItem label="通知标题" prop="title">
<Input v-model="formData.title" placeholder="请输入通知标题" maxlength="50" show-count />
</FormItem>
<!-- 发布范围 -->
<FormItem label="发布范围" prop="scope">
<div class="scope-selector">
<Select v-model="formData.scope.type" @change="handleScopeTypeChange">
<Option value="school">全校</Option>
<Option value="grade">年级</Option>
<Option value="class">班级</Option>
<Option value="student">指定学生</Option>
</Select>
<!-- 根据范围类型显示不同选择器 -->
<Select
v-if="formData.scope.type === 'grade'"
v-model="formData.scope.targetIds"
multiple
placeholder="选择年级"
>
<Option v-for="grade in grades" :key="grade.id" :value="grade.id">
{{ grade.name }}
</Option>
</Select>
<Cascader
v-if="formData.scope.type === 'class'"
v-model="formData.scope.targetIds"
:options="classOptions"
multiple
placeholder="选择班级"
/>
<StudentSelector
v-if="formData.scope.type === 'student'"
v-model="formData.scope.targetIds"
/>
</div>
</FormItem>
<!-- 通知内容 -->
<FormItem label="通知内容" prop="content">
<RichEditor
v-model="formData.content"
:max-length="5000"
placeholder="请输入通知内容..."
/>
</FormItem>
<!-- 附件上传 -->
<FormItem label="附件">
<FileUploader
v-model="formData.attachments"
:max-count="5"
:max-size="10"
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.png"
/>
</FormItem>
<!-- 确认设置 -->
<FormItem label="阅读确认">
<Checkbox v-model="formData.requireConfirm">
需要家长确认已读
</Checkbox>
<DatePicker
v-if="formData.requireConfirm"
v-model="formData.confirmDeadline"
type="datetime"
placeholder="确认截止时间(可选)"
/>
</FormItem>
<!-- 操作按钮 -->
<div class="form-actions">
<Button @click="handleSaveDraft">保存草稿</Button>
<Button @click="handlePreview">预览</Button>
<Button type="primary" @click="handlePublish">立即发布</Button>
</div>
</Form>
</div>
</template>
<script setup lang="ts">
const formData = reactive<AnnouncementForm>({
type: 'notice',
title: '',
content: '',
scope: {
type: 'class',
targetIds: []
},
attachments: [],
requireConfirm: false,
confirmDeadline: null
})
async function handlePublish() {
try {
await formRef.value.validate()
// 计算接收人数
const recipientCount = await calculateRecipients(formData.scope)
// 确认发布
const confirmed = await showConfirm({
title: '确认发布',
content: `该通知将发送给 ${recipientCount} 位家长,确认发布吗?`
})
if (!confirmed) return
// 发布通知
await publishAnnouncement(formData)
message.success('通知发布成功')
// 跳转到通知列表
router.push('/announcements')
} catch (error) {
message.error('发布失败:' + error.message)
}
}
</script>
通知阅读确认
// 通知确认服务
class AnnouncementConfirmService {
// 获取确认状态统计
async getConfirmStats(announcementId: string): Promise<ConfirmStats> {
const announcement = await this.getAnnouncement(announcementId)
const recipients = await this.getRecipients(announcement.scope)
const confirmRecords = await this.getConfirmRecords(announcementId)
return {
total: recipients.length,
read: confirmRecords.filter(r => r.readAt).length,
confirmed: confirmRecords.filter(r => r.confirmedAt).length,
unconfirmed: recipients.filter(r =>
!confirmRecords.find(c => c.parentId === r.parentId && c.confirmedAt)
)
}
}
// 发送催促确认通知
async sendReminder(announcementId: string, parentIds?: string[]) {
const stats = await this.getConfirmStats(announcementId)
const targets = parentIds || stats.unconfirmed.map(p => p.parentId)
// 批量发送提醒
await this.notifyService.batchSend({
type: 'reminder',
title: '请确认通知',
content: '您有一条未确认的通知,请及时查看并确认',
link: `/announcements/${announcementId}`,
targets
})
return { sent: targets.length }
}
// 家长确认通知
async confirmAnnouncement(announcementId: string, parentId: string) {
return await this.confirmRecordRepo.upsert({
announcementId,
parentId,
confirmedAt: new Date()
})
}
}
即时消息系统
消息服务实现
// 消息服务
class MessageService {
private wsConnections: Map<string, WebSocket> = new Map()
// 发送私信
async sendMessage(data: SendMessageInput): Promise<PrivateMessage> {
const { senderId, receiverId, content, messageType, attachments } = data
// 获取或创建会话
const conversation = await this.getOrCreateConversation(senderId, receiverId)
// 创建消息记录
const message = await this.messageRepo.create({
conversationId: conversation.id,
senderId,
senderName: await this.getUserName(senderId),
senderRole: await this.getUserRole(senderId),
receiverId,
receiverName: await this.getUserName(receiverId),
content,
messageType,
attachments,
createdAt: new Date()
})
// 更新会话最后消息
await this.conversationRepo.update(conversation.id, {
lastMessage: {
content: this.getMessagePreview(content, messageType),
senderId,
createdAt: message.createdAt
},
[`unreadCount.${receiverId}`]: (conversation.unreadCount[receiverId] || 0) + 1,
updatedAt: new Date()
})
// 实时推送给接收者
await this.pushToUser(receiverId, {
type: 'new_message',
data: message
})
// 发送离线推送通知
await this.sendPushNotification(receiverId, {
title: message.senderName,
body: this.getMessagePreview(content, messageType)
})
return message
}
// 获取消息列表
async getMessages(
conversationId: string,
pagination: PaginationParams
): Promise<PaginatedResult<PrivateMessage>> {
return await this.messageRepo.findByConversation(conversationId, {
...pagination,
orderBy: { createdAt: 'desc' }
})
}
// 标记消息已读
async markAsRead(conversationId: string, userId: string) {
// 更新消息已读状态
await this.messageRepo.updateMany(
{ conversationId, receiverId: userId, readAt: null },
{ readAt: new Date() }
)
// 清空未读计数
await this.conversationRepo.update(conversationId, {
[`unreadCount.${userId}`]: 0
})
// 通知发送方消息已读
const conversation = await this.conversationRepo.findById(conversationId)
const otherParticipant = conversation.participants.find(p => p.id !== userId)
if (otherParticipant) {
await this.pushToUser(otherParticipant.id, {
type: 'messages_read',
data: { conversationId, readerId: userId }
})
}
}
// WebSocket 连接管理
handleConnection(userId: string, ws: WebSocket) {
this.wsConnections.set(userId, ws)
ws.on('close', () => {
this.wsConnections.delete(userId)
})
ws.on('message', (data) => {
this.handleWebSocketMessage(userId, JSON.parse(data.toString()))
})
}
private async pushToUser(userId: string, payload: any) {
const ws = this.wsConnections.get(userId)
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(payload))
}
}
}
消息界面组件
<template>
<div class="message-chat">
<!-- 消息列表 -->
<div ref="messageListRef" class="message-list" @scroll="handleScroll">
<div v-if="loading" class="loading-more">
<Spin size="small" />
</div>
<div
v-for="(message, index) in messages"
:key="message.id"
class="message-item"
:class="{ 'is-self': message.senderId === currentUserId }"
>
<!-- 时间分割线 -->
<div
v-if="shouldShowTime(message, messages[index - 1])"
class="time-divider"
>
{{ formatMessageTime(message.createdAt) }}
</div>
<!-- 消息内容 -->
<div class="message-bubble">
<Avatar
v-if="message.senderId !== currentUserId"
:src="getParticipantAvatar(message.senderId)"
size="small"
/>
<div class="bubble-content">
<!-- 文本消息 -->
<div v-if="message.messageType === 'text'" class="text-content">
{{ message.content }}
</div>
<!-- 图片消息 -->
<div v-else-if="message.messageType === 'image'" class="image-content">
<img
:src="message.attachments[0].url"
@click="previewImage(message.attachments[0])"
/>
</div>
<!-- 语音消息 -->
<div v-else-if="message.messageType === 'voice'" class="voice-content">
<VoicePlayer :src="message.attachments[0].url" />
</div>
<!-- 文件消息 -->
<div v-else-if="message.messageType === 'file'" class="file-content">
<FileCard :file="message.attachments[0]" />
</div>
<span class="message-status">
<Icon v-if="message.readAt" name="check-double" class="read" />
<Icon v-else name="check" />
{{ formatTime(message.createdAt) }}
</span>
</div>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="message-input">
<div class="input-toolbar">
<Button text @click="showEmojiPicker = true">
<Icon name="smile" />
</Button>
<Button text @click="handleImageUpload">
<Icon name="image" />
</Button>
<Button text @click="handleFileUpload">
<Icon name="paperclip" />
</Button>
</div>
<Textarea
v-model="inputContent"
:rows="3"
placeholder="输入消息..."
@keydown.enter.ctrl="handleSend"
/>
<Button
type="primary"
:disabled="!inputContent.trim()"
@click="handleSend"
>
发送
</Button>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
conversationId: string
}>()
const messages = ref<PrivateMessage[]>([])
const inputContent = ref('')
const loading = ref(false)
// 加载消息
async function loadMessages(before?: Date) {
loading.value = true
try {
const result = await messageService.getMessages(props.conversationId, {
before,
limit: 20
})
if (before) {
messages.value = [...result.data, ...messages.value]
} else {
messages.value = result.data.reverse()
scrollToBottom()
}
} finally {
loading.value = false
}
}
// 发送消息
async function handleSend() {
if (!inputContent.value.trim()) return
const content = inputContent.value
inputContent.value = ''
// 乐观更新:先添加到列表
const tempMessage: PrivateMessage = {
id: `temp-${Date.now()}`,
conversationId: props.conversationId,
senderId: currentUserId.value,
senderName: currentUserName.value,
senderRole: 'parent',
receiverId: otherParticipant.value.id,
receiverName: otherParticipant.value.name,
content,
messageType: 'text',
createdAt: new Date()
}
messages.value.push(tempMessage)
scrollToBottom()
try {
const realMessage = await messageService.sendMessage({
receiverId: otherParticipant.value.id,
content,
messageType: 'text'
})
// 替换临时消息
const index = messages.value.findIndex(m => m.id === tempMessage.id)
if (index > -1) {
messages.value[index] = realMessage
}
} catch (error) {
// 发送失败,标记消息
const index = messages.value.findIndex(m => m.id === tempMessage.id)
if (index > -1) {
messages.value[index] = { ...tempMessage, failed: true }
}
}
}
// WebSocket 接收消息
onMounted(() => {
loadMessages()
wsService.on('new_message', (message: PrivateMessage) => {
if (message.conversationId === props.conversationId) {
messages.value.push(message)
scrollToBottom()
// 标记为已读
messageService.markAsRead(props.conversationId, currentUserId.value)
}
})
})
</script>
成长档案系统
成长记录模型
// 成长记录
interface GrowthRecord {
id: string
studentId: string
studentName: string
recordType: GrowthRecordType
title: string
content: string
attachments: Attachment[] // 照片、视频、文件
tags: string[] // 标签:如"运动"、"阅读"、"艺术"
visibility: 'public' | 'private' // 是否对家长可见
createdBy: {
id: string
name: string
role: 'teacher' | 'admin'
}
likes: number
comments: GrowthComment[]
createdAt: Date
}
type GrowthRecordType =
| 'daily' // 日常表现
| 'achievement' // 获得荣誉
| 'activity' // 活动参与
| 'milestone' // 成长里程碑
| 'work' // 作品展示
| 'evaluation' // 综合评价
// 成长评论
interface GrowthComment {
id: string
content: string
authorId: string
authorName: string
authorRole: 'teacher' | 'parent'
createdAt: Date
}
// 成长档案统计
interface GrowthSummary {
studentId: string
period: {
start: Date
end: Date
}
recordCount: number
achievementCount: number
activityCount: number
topTags: { tag: string; count: number }[]
monthlyTrend: { month: string; count: number }[]
}
成长档案展示
<template>
<div class="growth-portfolio">
<!-- 学生信息头部 -->
<div class="portfolio-header">
<Avatar :src="student.avatar" size="large" />
<div class="student-info">
<h2>{{ student.name }}</h2>
<p>{{ student.className }} | {{ student.studentNo }}</p>
</div>
<div class="quick-stats">
<StatCard label="成长记录" :value="summary.recordCount" icon="book" />
<StatCard label="获得荣誉" :value="summary.achievementCount" icon="trophy" />
<StatCard label="活动参与" :value="summary.activityCount" icon="calendar" />
</div>
</div>
<!-- 成长时间线 -->
<div class="growth-timeline">
<div class="timeline-filter">
<Select v-model="selectedType" placeholder="记录类型">
<Option value="">全部</Option>
<Option value="daily">日常表现</Option>
<Option value="achievement">获得荣誉</Option>
<Option value="activity">活动参与</Option>
<Option value="work">作品展示</Option>
</Select>
<RangePicker v-model="dateRange" />
</div>
<Timeline>
<TimelineItem
v-for="record in filteredRecords"
:key="record.id"
:color="getTypeColor(record.recordType)"
>
<template #dot>
<Icon :name="getTypeIcon(record.recordType)" />
</template>
<GrowthRecordCard
:record="record"
:can-edit="canEdit"
@like="handleLike"
@comment="handleComment"
/>
</TimelineItem>
</Timeline>
<div v-if="hasMore" class="load-more">
<Button @click="loadMore">加载更多</Button>
</div>
</div>
<!-- 成长统计图表 -->
<div class="growth-analytics">
<Card title="成长趋势">
<LineChart :data="summary.monthlyTrend" />
</Card>
<Card title="成长标签">
<TagCloud :tags="summary.topTags" />
</Card>
</div>
</div>
</template>
请假管理系统
请假申请流程
// 请假申请
interface LeaveRequest {
id: string
studentId: string
studentName: string
classId: string
className: string
applicantId: string // 申请人(家长)ID
applicantName: string
applicantRelation: ParentRelation
leaveType: LeaveType
startTime: Date
endTime: Date
duration: number // 请假时长(小时或天)
reason: string
attachments: Attachment[] // 证明材料
status: LeaveStatus
workflow: LeaveWorkflowStep[]
createdAt: Date
updatedAt: Date
}
type LeaveType = 'sick' | 'personal' | 'family' | 'other'
type LeaveStatus = 'pending' | 'approved' | 'rejected' | 'cancelled' | 'revoked'
interface LeaveWorkflowStep {
step: number
approverRole: 'teacher' | 'director' | 'principal'
approverId?: string
approverName?: string
action?: 'approve' | 'reject'
comment?: string
actionTime?: Date
}
// 请假服务
class LeaveService {
// 提交请假申请
async submitLeaveRequest(data: SubmitLeaveInput): Promise<LeaveRequest> {
// 计算请假时长
const duration = this.calculateDuration(data.startTime, data.endTime)
// 确定审批流程
const workflow = this.determineWorkflow(duration, data.leaveType)
// 创建请假记录
const request = await this.leaveRepo.create({
...data,
duration,
status: 'pending',
workflow,
createdAt: new Date()
})
// 通知第一级审批人
await this.notifyApprover(request, workflow[0])
return request
}
// 确定审批流程
private determineWorkflow(duration: number, type: LeaveType): LeaveWorkflowStep[] {
// 病假3天以上需要年级主任审批
// 7天以上需要校长审批
const steps: LeaveWorkflowStep[] = [
{ step: 1, approverRole: 'teacher' }
]
if (duration >= 3 || type === 'other') {
steps.push({ step: 2, approverRole: 'director' })
}
if (duration >= 7) {
steps.push({ step: 3, approverRole: 'principal' })
}
return steps
}
// 审批请假
async approveLeave(
requestId: string,
approverId: string,
action: 'approve' | 'reject',
comment?: string
): Promise<LeaveRequest> {
const request = await this.leaveRepo.findById(requestId)
// 找到当前待审批步骤
const currentStep = request.workflow.find(s => !s.action)
if (!currentStep) {
throw new Error('没有待审批的步骤')
}
// 验证审批权限
await this.validateApprover(approverId, currentStep.approverRole, request)
// 更新审批步骤
currentStep.approverId = approverId
currentStep.approverName = await this.getUserName(approverId)
currentStep.action = action
currentStep.comment = comment
currentStep.actionTime = new Date()
// 判断流程状态
if (action === 'reject') {
request.status = 'rejected'
} else {
const nextStep = request.workflow.find(s => !s.action)
if (nextStep) {
// 还有下一步审批
await this.notifyApprover(request, nextStep)
} else {
// 审批完成
request.status = 'approved'
}
}
request.updatedAt = new Date()
await this.leaveRepo.update(request)
// 通知家长审批结果
await this.notifyParent(request)
return request
}
}
家长会管理
家长会预约系统
<template>
<div class="parent-meeting">
<!-- 教师端:创建家长会 -->
<Card v-if="isTeacher" title="创建家长会">
<Form :model="meetingForm" :rules="meetingRules">
<FormItem label="会议主题" prop="title">
<Input v-model="meetingForm.title" />
</FormItem>
<FormItem label="会议时间" prop="timeRange">
<RangePicker
v-model="meetingForm.timeRange"
type="datetime"
:disabled-date="disablePastDates"
/>
</FormItem>
<FormItem label="会议形式" prop="format">
<RadioGroup v-model="meetingForm.format">
<Radio value="offline">线下会议</Radio>
<Radio value="online">线上会议</Radio>
<Radio value="hybrid">混合模式</Radio>
</RadioGroup>
</FormItem>
<FormItem v-if="meetingForm.format !== 'online'" label="会议地点" prop="location">
<Input v-model="meetingForm.location" />
</FormItem>
<FormItem label="预约设置">
<Checkbox v-model="meetingForm.requireBooking">
需要家长预约时间段
</Checkbox>
</FormItem>
<FormItem v-if="meetingForm.requireBooking" label="时间段设置">
<TimeSlotEditor v-model="meetingForm.timeSlots" />
</FormItem>
<Button type="primary" @click="createMeeting">创建家长会</Button>
</Form>
</Card>
<!-- 家长端:预约时间 -->
<Card v-if="isParent && activeMeeting" title="预约家长会">
<div class="meeting-info">
<h3>{{ activeMeeting.title }}</h3>
<p>
<Icon name="calendar" />
{{ formatDateRange(activeMeeting.startTime, activeMeeting.endTime) }}
</p>
<p>
<Icon name="location" />
{{ activeMeeting.format === 'online' ? '线上会议' : activeMeeting.location }}
</p>
</div>
<div class="time-slot-picker">
<h4>选择时间段</h4>
<div class="slots-grid">
<div
v-for="slot in availableSlots"
:key="slot.id"
class="slot-item"
:class="{
'is-selected': selectedSlot === slot.id,
'is-booked': slot.booked,
'is-mine': slot.bookedBy === currentParentId
}"
@click="selectSlot(slot)"
>
<span class="slot-time">{{ slot.startTime }} - {{ slot.endTime }}</span>
<span v-if="slot.booked && slot.bookedBy !== currentParentId" class="slot-status">
已预约
</span>
<span v-if="slot.bookedBy === currentParentId" class="slot-status mine">
我的预约
</span>
</div>
</div>
</div>
<div class="booking-actions">
<Button
v-if="myBooking"
@click="cancelBooking"
>
取消预约
</Button>
<Button
v-else
type="primary"
:disabled="!selectedSlot"
@click="confirmBooking"
>
确认预约
</Button>
</div>
</Card>
</div>
</template>
推送通知系统
多渠道通知服务
// 通知服务
class NotificationService {
private channels: NotificationChannel[] = []
constructor() {
this.channels = [
new AppPushChannel(),
new SMSChannel(),
new WeChatChannel(),
new EmailChannel()
]
}
async send(notification: NotificationPayload) {
const { recipients, ...payload } = notification
// 获取每个接收者的通知偏好
for (const recipientId of recipients) {
const settings = await this.getNotifySettings(recipientId)
const contact = await this.getContactInfo(recipientId)
// 检查免打扰时段
if (this.isQuietHour(settings)) {
await this.scheduleForLater(recipientId, payload)
continue
}
// 按渠道发送
const promises = []
if (settings.pushEnabled) {
promises.push(this.sendViaChannel('push', contact.pushToken, payload))
}
if (settings.smsEnabled && this.isHighPriority(payload)) {
promises.push(this.sendViaChannel('sms', contact.phone, payload))
}
if (contact.wechatOpenId) {
promises.push(this.sendViaChannel('wechat', contact.wechatOpenId, payload))
}
await Promise.allSettled(promises)
}
}
private async sendViaChannel(
channelType: string,
target: string,
payload: NotificationPayload
) {
const channel = this.channels.find(c => c.type === channelType)
if (!channel) return
try {
await channel.send(target, payload)
await this.logNotification(channelType, target, payload, 'success')
} catch (error) {
await this.logNotification(channelType, target, payload, 'failed', error)
}
}
}
// 微信模板消息通道
class WeChatChannel implements NotificationChannel {
type = 'wechat'
async send(openId: string, payload: NotificationPayload) {
const templateId = this.getTemplateId(payload.type)
await this.wechatApi.sendTemplateMessage({
touser: openId,
template_id: templateId,
miniprogram: {
appid: process.env.MINIPROGRAM_APPID,
pagepath: payload.link
},
data: this.formatTemplateData(payload)
})
}
private formatTemplateData(payload: NotificationPayload) {
return {
first: { value: payload.title },
keyword1: { value: payload.body },
keyword2: { value: formatDate(new Date()) },
remark: { value: '点击查看详情' }
}
}
}
总结
家校沟通平台的设计要点:
- 多样化沟通方式:支持公告、私信、评论等多种沟通形式
- 精准的消息触达:多渠道推送确保消息送达
- 完善的成长记录:记录学生成长点滴,促进家校共育
- 便捷的事务处理:请假、缴费等流程线上化
- 实时的互动反馈:WebSocket 实现即时通讯
通过系统化的设计,可以构建出高效、便捷的家校沟通平台,促进教育协作。


