别再死记硬背电路图了!手把手教你用MOSFET和二极管‘拼’出Buck变换器的核心开关
2026/6/9 10:53:42
说明:本说明基于自己毕设项目中“系统通知模块 (Notification / SSE)”的实现,重点讲清楚在前端从 **初始化环境 → 建立 SSE 连接 → 解析服务端事件 → 打印日志 ** 的完整技术链路,至于收到信息如何处理和具体项目有关。
在我的毕设中,又系统通知这个功能,单向的,告诉用户,你的内容发布审核成功,或者失败等等一系列的单方面的通知,所有选型sse而不选择websorket
接下来先给出后端实现,在是前端实现
涉及的核心类:
NotificationController:通知相关 HTTP 接口(SSE 流、已读、未读数、分页列表)。NotificationService:通知的持久化、SSE 连接管理与推送实现。Notification/notification表:通知实体与数据库表结构。NotificationType/NotificationStatus:通知类型与状态枚举(如PRIVATE_MESSAGE)。GET /api/notify/streamNotificationControllerstream(@RequestHeader(value = "Last-Event-ID", required = false) String lastEventId)/api/notify/streamUserContext.getUserId()读取 userId)。401 UNAUTHORIZED。notificationService.connect(userId, lastEventId)建立 SSE 连接。简要流程:
UserContext获取当前用户 ID。ResponseStatusException(HttpStatus.UNAUTHORIZED)。userId与Last-Event-ID交给NotificationService.connect。SseEmitter对象,由框架维持 SSE 链接。NotificationService.connectpublic SseEmitter connect(Long userId, String lastEventId)SseEmitter实例,超时时间为SSE_TIMEOUT = 30 * 60 * 1000L(30 分钟)。SseEmitter加入到emitterPool中:emitterPool类型为Map<Long, CopyOnWriteArrayList<SseEmitter>>,按userId维护用户当前所有 SSE 连接。emitter注册回调:onCompletion/onTimeout/onError时,从emitterPool中移除当前连接。heartbeat),名称为"heartbeat",数据为"ping"。Last-Event-ID:lastEventId,解析为lastId(Long)。null。resendPending(userId, lastId, emitter)补发历史通知(最多 100 条)。SseEmitter,等待后续通知推送。connect方法中,对SseEmitter注册了:onCompletion:连接正常完成时调用removeEmitter(userId, emitter)。onTimeout:连接超时时调用removeEmitter(userId, emitter)。onError:发送异常等错误时调用removeEmitter(userId, emitter)。removeEmitter会从emitterPool对应用户的列表中移除该SseEmitter,防止内存泄漏与后续重复推送。NotificationService.createAndDispatch方法签名:
publicNotificationcreateAndDispatch(LonguserId,NotificationTypetype,Stringtitle,Stringcontent,Stringpayload)调用方示例:
ChatServiceImpl)。执行流程:
buildNotification(userId, type, title, content, payload)构造一个Notification实体:userId:接收通知的用户 ID。type:通知类型(如PRIVATE_MESSAGE、SYSTEM_ANNOUNCEMENT等)。title:通知标题/摘要。content:通知正文或简述。payload:扩展 JSON 字符串,用于前端跳转或展示更多信息。status:初始为UNREAD。createdAt/updatedAt:为当前时间。notificationMapper.insert(notification)将通知写入notification表。dispatch(notification)将该通知推送给所有当前在线的 SSE 连接。Notification对象。createAndDispatchBatch方法签名:
publicvoidcreateAndDispatchBatch(List<Long>userIds,NotificationTypetype,Stringtitle,Stringcontent,Stringpayload)逻辑:对userIds逐个调用createAndDispatch,用于对多用户广播同一类型通知(如系统公告或活动推送)。
NotificationService.dispatchpublic void dispatch(Notification notification)emitterPool中取出该通知对应userId的所有SseEmitter列表。SseEmitter,调用:emitter.send(SseEmitter.event().id(String.valueOf(notification.getId())).name(notification.getType()).data(notification)):id:使用通知 ID(字符串形式),用于客户端的Last-Event-ID与断点续传。name:使用notification.getType(),即NotificationType的枚举名(如PRIVATE_MESSAGE)。data:整个Notification对象(前端接收后可按NotificationVO解析)。IOException,记录日志并调用removeEmitter移除当前失效连接。NotificationService.resendPendingprivate void resendPending(Long userId, Long lastId, SseEmitter emitter)lastId != null:补发ID 大于lastId的所有通知。lastId == null:补发所有未读(status = UNREAD)通知。LIMIT 1条,只是补发过最新一条数据,列表通过数据库查询。SseEmitter.event().id(...).name(...).data(...)发送,与实时推送一致。POST /api/notify/readNotificationController.markRead/api/notify/readNotificationReadRequest,包含:notificationIds(可选):要标记为已读的通知 ID 列表。upToId(可选):将id <= upToId的通知全部标记为已读。UserContext获取当前用户 ID,未登录返回Result.unauthorized("未登录")。notificationService.markAsRead(userId, notificationIds, upToId)执行更新。notificationService.countUnread(userId)获取最新未读数量。Result.success(unread)。NotificationService.markAsReadpublic void markAsRead(Long userId, List<Long> notificationIds, Long upToId)notificationIds为空且upToId == null,直接返回,不做更新。LambdaUpdateWrapper<Notification>:eq(Notification::getUserId, userId):只更新当前用户的通知。eq(Notification::getStatus, NotificationStatus.UNREAD.name()):只处理未读通知。notificationIds不为空:in(Notification::getId, notificationIds)。upToId不为null:le(Notification::getId, upToId)。set(Notification::getStatus, NotificationStatus.READ.name())。notificationMapper.update(null, wrapper)完成批量更新。GET /api/notify/unread-countNotificationController.unreadCount/api/notify/unread-countUserContext获取当前用户 ID,未登录返回Result.unauthorized("未登录")。notificationService.countUnread(userId)获取未读数量。Result.success(unread)。NotificationService.countUnreadpublic long countUnread(Long userId)LambdaQueryWrapper<Notification>:eq(Notification::getUserId, userId)eq(Notification::getStatus, NotificationStatus.UNREAD.name())notificationMapper.selectCount(wrapper)返回未读总数。GET /api/notify/recent/api/notify/recentpublic List<Notification> recentUnread(Long userId)userId匹配当前用户;status = UNREAD;createdAt倒序;last("LIMIT 100")限制数量。GET /api/notify/listNotificationController.list/api/notify/listpage:页码,默认1。pageSize:每页数量,默认10。status:可选,UNREAD/READ/ALL,默认ALL(不传时也视为ALL)。UserContext获取当前用户 ID,未登录返回Result.unauthorized("未登录")。notificationService.listNotifications(userId, page, pageSize, status)。Result.success(Page<NotificationVO>),分页结构与项目统一(total/size/current/pages/records等)。NotificationService.listNotificationspublic Page<NotificationVO> listNotifications(Long userId, int page, int pageSize, String status)Page<Notification> pageParam = new Page<>(page, pageSize)。LambdaQueryWrapper<Notification> wrapper:eq(Notification::getUserId, userId)。status非空且不是"ALL":eq(Notification::getStatus, status.toUpperCase())。orderByDesc(Notification::getCreatedAt)。notificationMapper.selectPage(pageParam, wrapper)得到notificationPage。Page<NotificationVO> voPage = new Page<>(page, pageSize, notificationPage.getTotal()):notificationPage.getRecords(),将每条记录映射为NotificationVO:id/userId/type/title/content/payload/status/createdAt等字段。voPage.setRecords(records)。voPage。当用户 A 给用户 B 发送一对一私信时:
chat:online:{userId}标记):/topic/chat/{sessionId})。chat_message表的基础上,额外为 B 创建一条PRIVATE_MESSAGE类型的系统通知,写入notification表并通过 SSE 推送/补发。ChatServiceImpl.sendMessageChatMessage并更新ChatSession.updatedAt后:"single".equalsIgnoreCase(session.getSessionType())。ChatSessionMember,userId != 发送者) 获取targetUserId。RedisConstants.CHAT_ONLINE_PREFIX + targetUserId从 Redis 中读取在线标记:senderName。title = "收到一条新的私信"。preview = content的前若干字符(超长截断)。payloadJSON 字符串:包含sessionId、messageId、senderId、senderName,供前端跳转使用。notificationService.createAndDispatch(targetUserId, NotificationType.PRIVATE_MESSAGE, title, preview, payload):notification表。PRIVATE_MESSAGE事件。resendPending收到历史通知。vite.config.js/api→ 后端VITE_API_BASE_URL。src/main.jssrc/utils/notificationStream.jssrc/stores/notification.jssrc/components/Header/index.vuesrc/pages/UserCenter/pages/Notifications/index.vuevite.config.js中开发环境的核心代理配置:
server:{port:3003,host:true,cors:true,...(isDev&&{proxy:{'/api':{target:env.VITE_API_BASE_URL||'http://localhost:8080',changeOrigin:true,rewrite:(path)=>path.replace(/^\/api/,'')}}})}关键点:
http://localhost:3003(前端 dev server)。/api开头的请求都会被 Vite 转发到VITE_API_BASE_URL对应的后端,例如:/api/api/notify/stream/api,转成/api/notify/streamhttp://127.0.0.1:8081/api/notify/stream(和后端文档一致)。src/main.js在main.js中:
constapp=createApp(App)app.use(pinia)app.use(router)// ...constuserStore=useUserStore()constnotificationStore=useNotificationStore()userStore.initUserState()if(userStore.isLogin){initNotificationStream()notificationStore.fetchUnreadCount()}watch(()=>userStore.isLogin,(isLogin)=>{if(isLogin){initNotificationStream()notificationStore.fetchUnreadCount()}else{closeNotificationStream()notificationStore.reset()}})结论:
notificationStream.js中的连接与解析/api/api/notify/stream说明:
http://localhost:3003/api/api/notify/stream(同源)。/api→/api/notify/stream。http://127.0.0.1:8081/api/notify/stream(与接口文档一致)。initNotificationStream负责发起连接:
exportfunctioninitNotificationStream(){if(typeofwindow==='undefined')returnif(reading)return// 已在读流则不重复建立constuserStore=useUserStore()constnotificationStore=useNotificationStore()try{constsseUrl='/api/api/notify/stream'console.log('[SSE] 准备建立连接:',sseUrl)abortController=newAbortController()reading=truenotificationStore.setSseConnected(true)fetch(sseUrl,{method:'GET',headers:{Accept:'text/event-stream','Cache-Control':'no-cache',...(userStore.token?{Authorization:`Bearer${userStore.token}`}:{})},credentials:'include',signal:abortController.signal}).then(/* 处理响应与读流 */).catch(/* 错误处理 */)}catch(error){// ...}}关键点:
fetch而不是EventSource,是因为需要自定义Authorization头。Authorization: Bearer <token>携带登录状态,兼容后端鉴权逻辑。credentials: 'include'保留 Cookie 信息(如果后端需要)。.then(async(response)=>{console.log('[SSE] 响应状态:',response.status,response.statusText)console.log('[SSE] 响应头 content-type:',response.headers.get('content-type'))if(!response.ok||!response.body){thrownewError(`SSE 连接失败:${response.status}`)}constreader=response.body.getReader()constdecoder=newTextDecoder('utf-8')letbuffer=''// 持续读流...})此时在浏览器控制台可以看到:
[SSE] 响应状态: 200 OK[SSE] 响应头 content-type: text/event-stream;charset=UTF-8核心循环逻辑:
while(true){const{done,value}=awaitreader.read()if(done){console.log('[SSE] 流已结束')break}buffer+=decoder.decode(value,{stream:true})console.log('[SSE] 收到原始 chunk:',buffer)// 根据 \n\n 切分事件块constevents=buffer.split('\n\n')buffer=events.pop()||''for(constrawEventofevents){constlines=rawEvent.split('\n')leteventType='message'letdata=''letlastEventId=nullfor(constlineoflines){if(line.startsWith('event:')){eventType=line.slice(6).trim()}elseif(line.startsWith('data:')){data+=line.slice(5).trim()}elseif(line.startsWith('id:')){lastEventId=line.slice(3).trim()}}console.log('[SSE] 解析到事件:',{eventType,lastEventId,data,rawEvent})if(lastEventId){notificationStore.setLastEventId(lastEventId)}if(eventType==='heartbeat')continueif(!data)continuetry{constparsed=JSON.parse(data)notificationStore.handleIncomingNotification(parsed)}catch(error){console.error('解析通知 SSE 消息失败:',error,data)}}}控制台能看到的典型日志(以 PRIVATE_MESSAGE 为例):
[SSE] 收到原始 chunk: id:21\nevent:PRIVATE_MESSAGE\ndata:{"id":21,"userId":3,"type":"PRIVATE_MESSAGE",...}\n\n[SSE] 解析到事件: { eventType: 'PRIVATE_MESSAGE', lastEventId: '21', data: '{"id":21,"userId":3,"type":"PRIVATE_MESSAGE",...}', rawEvent: 'id:21\nevent:PRIVATE_MESSAGE\ndata:{"id":21,...}' }文件:src/stores/notification.js
state:()=>({unreadCount:0,notifications:[],pagination:{total:0,size:10,current:1,pages:0},loadingList:false,loadingUnread:false,sseConnected:false,lastEventId:null})handleIncomingNotification(notification){if(!notification||!notification.id){// 当前约定:必须有 id 才认为是有效通知return}constexists=this.notifications.some(item=>item.id===notification.id)if(!exists){this.notifications=[notification,...this.notifications]}// 未显式标记为 READ 的,都算未读if(!notification.status||notification.status==='UNREAD'){this.unreadCount+=1}}效果:
id字段)会被添加到notifications列表顶部。unreadCount会自增,用于 Header 红点等 UI 展示。文件:src/components/Header/index.vue
constnotificationStore=useNotificationStore()consthasUnread=computed(()=>notificationStore.unreadCount>0)在模板中:
<button class="gridButton" @click="handleNotificationClick"> <span style="position: relative; display: inline-block;"> <i class="fas fa-bell text-lg"></i> <span v-if="hasUnread" class="unreadBadge"></span> </span> </button>当 SSE 收到任意一条未读通知:
handleIncomingNotification→unreadCount++hasUnread变为trueunreadBadge渲染,小红点点亮。文件:src/pages/UserCenter/pages/Notifications/index.vue
constnotificationStore=useNotificationStore()constnotifications=computed(()=>notificationStore.notifications)constloading=computed(()=>notificationStore.loadingList)首次进入页面时,会从后端拉一次历史通知列表:
onMounted(()=>{notificationStore.fetchNotifications({page:1,pageSize:10,status:'ALL'})})当 SSE 推送新通知时:
notifications首元素会是最新一条;/api/notify/stream→NotificationService.connect→ 维护emitterPool+ 心跳 + 历史补发。NotificationService.createAndDispatch,将通知写入notification表并尝试通过 SSE 推送。Last-Event-ID或未读筛选补发(最多 100 条)。/api/notify/read和/api/notify/unread-count负责已读标记与未读统计。/api/notify/recent提供最近未读调试接口;/api/notify/list提供按状态过滤的分页通知列表。ChatServiceImpl在单聊离线场景下,为接收方生成PRIVATE_MESSAGE通知,实现“有人给你发私信”类型的系统提示。