news 2026/9/14 2:31:51

基于JSP的共享笔记系统:权限控制与数据建模全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于JSP的共享笔记系统:权限控制与数据建模全解析

简介:这套基于 JSP 的共享笔记系统毕业设计资料,面向高校计算机相关专业学生,用于完成课程设计或毕业设计选题。系统围绕笔记管理与共享展开,除标签搜索外,还提供用户管理、笔记公开、文本共享数据存储模块,并通过权限控制机制细化访问粒度,适合学习 JSP/Servlet、Hibernate、权限设计及前后端整合的开发者参考。压缩包共 414 个文件,涵盖 Java/JSP 源码、配置文件、HTML/CSS/JS 页面资源、数据库 SQL 脚本以及设计报告文档,其中 19 个 Java 类与 18 个 JSP 页面清晰呈现业务分层,jar 包保证环境可运行,数据库脚本便于快速还原初始数据,整体约 25.85MB。已有 274 人浏览学习。资料内附完整项目代码、数据库及配套报告,代码注释与目录结构相对完整,可直接导入 IDE 运行验证,也可按模块拆解学习,为毕业设计答辩和二次开发提供落地参考。

1. 为什么共享笔记系统还值得用 JSP 做一次

在很多毕业设计清单里,基于 JSP 的共享笔记系统常常被当成一个“凑数的老技术”,但真把这个题目接住的人会发现,共享笔记的难点不在写笔记,而在“共享”:谁能看、谁能改、分享链接有效期多长、以及多人同时打开时页面会不会串数据。用 JSP + Servlet + MySQL 实现这套逻辑,正好能把权限控制和数据访问都放在服务端做,前面再配 JSP 页面做简单的模板渲染,整条链路短、依赖少,答辩时反而容易讲清楚。这篇文章会按“数据建模 → Servlet 流程 → JSP 编译与页面细节 → 权限验证”的顺序,把一份能跑通的方案拆给你看,覆盖从建表到部署验证的完整闭环。

2. 共享笔记的数据建模:用户、笔记、共享权限怎么在 MySQL 里落地

2.1 选型:Eclipse + Tomcat + MySQL 的取舍理由

做共享笔记系统的常见组合是 Eclipse / IDEA 配 Tomcat 8.5,数据库用 MySQL 5.7,JDK 用 1.8,连接池用 c3p0 或 DBCP2。为什么不用 Spring Boot?不是不能用,而是毕业设计场景里,JSP + Servlet 这种“请求进来 → Servet 处理 → 转发到 JSP 渲染”的模式,每一步都能在答辩时切到源码讲清楚,不会像框架封装那样被追问底层时卡住。

共享笔记的核心诉求是“多用户数据隔离 + 受控共享”,所以表结构必须先定明白。很多同学直接把笔记表里加一个shared_with字段,用逗号分隔用户 ID,这是最常见的错误。后续要做过期时间、读写权限、查看谁分享了这篇笔记,都会变得极其痛苦。正确做法是用独立的共享关系表。

2.2 note 表与 share 表的设计:字段、约束与索引

用户表user没什么特殊,关键是笔记表note和共享表share要拆开。笔记表只存笔记内容和归属人,是否公开用is_public控制;共享表记录“某篇笔记被共享给了谁、权限是什么、token 是什么、什么时候过期”。SQL 建表如下。

CREATE TABLE user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password_md5 CHAR(32) NOT NULL, avatar VARCHAR(128), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE note ( note_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, title VARCHAR(128) NOT NULL, content TEXT, is_public TINYINT DEFAULT 0, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_note_user FOREIGN KEY (user_id) REFERENCES user(user_id) ); CREATE TABLE share ( share_id INT PRIMARY KEY AUTO_INCREMENT, note_id INT NOT NULL, owner_id INT NOT NULL, target_user_id INT, permission ENUM('read', 'write') DEFAULT 'read', share_token VARCHAR(32) NOT NULL, expires_at DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_share_note FOREIGN KEY (note_id) REFERENCES note(note_id), CONSTRAINT fk_share_owner FOREIGN KEY (owner_id) REFERENCES user(user_id) ); CREATE INDEX idx_share_token ON share(share_token); CREATE INDEX idx_note_user ON note(user_id);

几个容易被忽略的参数说明:share_token用 UUID 去掉横线后取前 32 位,作为公开链接的唯一凭证;expires_at允许为空,为空表示永久有效;permission用 ENUM 而不是 TINYINT,是为了在代码里直接映射枚举,避免数字代表含义写死。owner_id虽然在note.user_id里已经存在,但共享表里冗余一份,是为了方便做“我共享出去的所有笔记”这类反向查询,不用每次 join note 表。索引加在share_token上,因为共享链接访问是最频繁的查询路径。

2.3 DAO 层封装:用连接池管理共享访问的连接

每个请求都要拿数据库连接的场景,直接DriverManager.getConnection会频繁建立 TCP 连接,Tomcat 下并发一高页面就卡。常见的做法是用 c3p0,配置文件c3p0-config.xml里给连接池定最大连接数、超时时间。下面的DBUtil是所有 DAO 的入口。

import com.mchange.v2.c3p0.ComboPooledDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class DBUtil { private static DataSource ds = new ComboPooledDataSource("mysql"); public static Connection getConnection() throws SQLException { return ds.getConnection(); } }

ComboPooledDataSource("mysql")会从c3p0-config.xml中读取名为mysql的配置。连接池的三个关键参数:initialPoolSize设置启动时建几个连接,maxPoolSize决定峰值连接数,checkoutTimeout控制拿不到连接时等多久。共享笔记系统里共享链接可能被同时打开很多次,maxPoolSize建议设到 30,checkoutTimeout设 3000 毫秒,超过 3 秒直接报“连接获取超时”,总比页面挂死强。

NoteDAO 中addNote方法是最基础也最典型的写法,注意is_public参数必须从布尔型转成 int,PreparedStatement 的索引从 1 开始,顺序与 SQL 里的?完全一致。

public int addNote(Note note) { String sql = "INSERT INTO note(user_id, title, content, is_public) VALUES(?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setInt(1, note.getUserId()); ps.setString(2, note.getTitle()); ps.setString(3, note.getContent()); ps.setInt(4, note.isPublic() ? 1 : 0); ps.executeUpdate(); ResultSet rs = ps.getGeneratedKeys(); return rs.next() ? rs.getInt(1) : -1; } catch (SQLException e) { throw new RuntimeException("保存笔记失败", e); } }

这里用try-with-resources让连接、语句、结果集在方法结束后自动关闭,避免连接泄漏。RETURN_GENERATED_KEYS让 MySQL 自增主键可以立刻回读,后续做共享关系时直接拿这个noteId插入 share 表。

3. 核心流程的 Servlet 实现:编辑笔记、共享链接与 JSP 页面数据回显

3.1 MVC 分工:JSP、Servlet、JavaBean 各自的边界

共享笔记系统里,Servlet 只做三件事:解析请求参数、调用 DAO、把结果放进 request 或 session。JSP 页面负责用 EL 表达式和 JSTL 标签把数据渲染成 HTML,不要在 JSP 里写try-catch、不要做数据库操作、不要用<% %>包一大段业务代码。常见的问题是同学把查询用户信息的代码直接写进 JSP 页面顶部,页面一多代码重复率极高,改一个表名字段得把所有页面翻一遍。下面的 ShareServlet 处理“把笔记共享给指定用户”的请求。

@WebServlet("/share/add") public class ShareAddServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); HttpSession session = req.getSession(); Integer userId = (Integer) session.getAttribute("userId"); if (userId == null) { resp.sendRedirect("/login.jsp"); return; } int noteId = Integer.parseInt(req.getParameter("noteId")); String targetUsername = req.getParameter("targetUsername"); String permission = req.getParameter("permission"); String expireDays = req.getParameter("expireDays"); UserDAO userDAO = new UserDAO(); Integer targetUserId = userDAO.findIdByUsername(targetUsername); if (targetUserId == null) { req.setAttribute("message", "目标用户不存在"); req.getRequestDispatcher("/shareAdd.jsp").forward(req, resp); return; } NoteDAO noteDAO = new NoteDAO(); Note note = noteDAO.findById(noteId); if (note == null || note.getUserId() != userId) { resp.sendError(403, "只能共享自己的笔记"); return; } String token = UUID.randomUUID().toString().replace("-", ""); ShareDAO shareDAO = new ShareDAO(); shareDAO.addShare(noteId, userId, targetUserId, permission, token, Integer.parseInt(expireDays)); resp.sendRedirect("/note/view?id=" + noteId); } }

参数说明:targetUsername是一次性输入的用户名,后端再转成target_user_id,前端不要传用户 ID,否则别人改一下表单值就能把笔记共享给任意用户。expireDays以字符串接收,在 DAO 层使用DATE_ADD(NOW(), INTERVAL ? DAY)生成过期时间。注意note.getUserId() != userId是比较两个 int 的基本类型,这里的细节是userId从 session 取出后可能是 null,必须先判空再拆箱,否则会抛 NullPointerException。

3.2 noteForm.jsp 中编辑笔记的防越权控制

编辑页面的 JSP 核心逻辑是:只有笔记作者能看到“保存”按钮,被共享的有写权限用户也能看到,但只读用户只能看到内容。这里把当前登录用户 ID 从 session 中取出,与笔记的user_id对比,页面里用 JSTL 条件标签控制按钮显隐。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <div class="note-header"> <h2>${note.title}</h2> <span class="meta">最后更新:${note.updateTime}</span> <c:if test="${sessionScope.userId == note.userId}"> <a href="/note/edit?id=${note.noteId}" class="btn-edit">编辑</a> </c:if> <c:if test="${sessionScope.userId != note.userId && noteEditable}"> <a href="/note/edit?id=${note.noteId}" class="btn-edit">协同编辑</a> </c:if> </div>

noteEditable是 Servlet 根据 share 表的 permission 字段放入 request 的布尔值,页面只读取结果,不自己查表。这里最容易踩的坑:用${sessionScope.userId != note.userId}判断权限,但这个写法在两者类型不一致时会永远为 true。正确做法是在 Servlet 里把userId转成 int,再setAttribute到 request 中,JSP 里只对比 request 属性,不要在页面上做类型转换。

3.3 共享链接的实现逻辑:token 是无登录入口

共享链接的定位是“无需登录,拿到链接就能看”。所以校验不能依赖 session,必须靠share_token。用户点击类似/share/view?token=abc123的地址时,ShareViewServlet 拿着 token 查 share 表,查不到或已过期就返回 404,否则把 note 内容放进 request 后转发到sharedNote.jsp

@WebServlet("/share/view") public class ShareViewServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String token = req.getParameter("token"); if (token == null || token.trim().isEmpty()) { resp.sendError(404); return; } ShareDAO shareDAO = new ShareDAO(); SharedNote shared = shareDAO.findByToken(token); if (shared == null) { resp.sendError(404, "共享链接不存在"); return; } if (shared.getExpiresAt() != null && shared.getExpiresAt().before(new Date())) { resp.sendError(404, "共享链接已过期"); return; } req.setAttribute("shared", shared); req.getRequestDispatcher("/sharedNote.jsp").forward(req, resp); } }

findByToken的 SQL 要 join note 表和 user 表,一次性把笔记内容和作者名都查出来,不要在 JSP 里再写一个 DAO 调用。过期时间判断放在数据库查询之后,用 Java 的Date.before()比较,注意expires_at为 null 时表示永久有效,必须先判空。sharedNote.jsp页面顶部要显示作者、共享时间、过期时间,同时提供一个只读标记,隐藏所有表单按钮。这里不校验登录状态,意味着任何人都能通过链接访问,所以 token 的随机性就特别重要,用 UUID 而不是自增 ID 能有效防遍历。

3.4 JSP 页面中正确 import JSONArray 处理共享成员列表

共享笔记系统里,作者需要看到“谁有权限访问这篇笔记”。这个列表可以通过 JSP 页面顶部用<%@ page import="net.sf.json.JSONArray" %>引入 JSON 工具类,然后直接把 List 对象转成 JSON 字符串输出到隐藏域,前端 JS 再解析。这种写法虽然不太优雅,但在 JSP 为主的技术栈里是最直接的。

<%@ page import="java.util.List" %> <%@ page import="net.sf.json.JSONArray" %> <%@ page import="com.example.vo.SharedUser" %> <% Integer currentUserId = (Integer) session.getAttribute("userId"); List<SharedUser> sharedUsers = new ShareDAO() .listSharedUsers((Integer) request.getAttribute("noteId"), currentUserId); JSONArray jsonArray = JSONArray.fromObject(sharedUsers); request.setAttribute("sharedUsersJson", jsonArray.toString()); %> <script> const sharedUsers = JSON.parse('${sharedUsersJson}'); sharedUsers.forEach(function (item) { console.log(item.username + ' - ' + item.permission); }); </script>

JSONArray.fromObject会把 JavaBean 里的 getter 方法自动映射成 JSON 字段,所以SharedUser类必须有getUsername()getPermission()这些标准 getter,否则输出会是空的{}listSharedUsers方法里要 join share 表和 user 表,按share.create_time倒序,取usernamepermissionexpires_at三个字段。注意 JSP 页面里写<% %>片段时,page import必须在页面最顶部,并且这段脚本不要放在 HTML body 中间,否则会跟表格渲染交叉导致页面变形。json-lib 这个库依赖 commons-beanutils 和 commons-lang,Tomcat 的 lib 目录和 WEB-INF/lib 都放一份最稳妥。

4. 排除 JSP 编译与页面交互的两个高频问题

4.1 JSP 编译后的 class 文件保存在哪里,以及怎么查看

JSP 不是直接运行的,Tomcat 首次访问 jsp 文件时,会先将其翻译成 Java Servlet 源码,再编译成 class 文件。源码和 class 都存放在 Tomcat 的work目录下,具体路径是work/Catalina/localhost/你的工程名/org/apache/jsp,例如web项目配置tomcat后查看jsp编译后的java类时看到的是noteForm_jsp.javanoteForm_jsp.class。查找这个文件可以解决很多诡异问题:页面报“变量找不到”,直接打开noteForm_jsp.java_jspService方法里到底生成了什么代码,就知道是 EL 表达式写错还是 request 属性没传进来。

修改 JSP 后 Tomcat 是否自动重新编译,取决于web.xml里 JspServlet 的配置。开发阶段把development参数保持为true,并设置checkInterval为 1 秒,这样每次请求都会检查 JSP 是否有修改,发现问题即时生效。上线前再把development改为false,并把trimSpaces设为true,可以去掉 JSP 指令和标签之间的多余空格,减小响应体积。

<servlet> <servlet-name>jsp</servlet-name> <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> <init-param> <param-name>development</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>checkInterval</param-name> <param-value>1</param-value> </init-param> <init-param> <param-name>trimSpaces</param-name> <param-value>true</param-value> </init-param> </servlet>

如果修改 JSP 后刷新页面还是旧内容,优先去work目录里找对应 jsp 文件的时间戳。时间戳没变,说明 Tomcat 没有感知到文件变化,重启 Tomcat 清一次 work 目录即可;时间戳变了但页面还是旧内容,那就是浏览器缓存,加一次 Ctrl+F5 强制刷新。

4.2 屏蔽 JSP 离开页面提示:在个人信息展示页面里处理未保存编辑

共享笔记系统中,个人信息展示页面(profileView.jsp)会展示用户最近编辑的笔记列表,点击后跳转到笔记编辑页。一个典型场景是:用户在编辑页写了半篇笔记,没有保存就点击其他链接或关闭标签页,此时需要弹窗提示“离开后未保存内容将丢失”。用beforeunload事件能做到,但问题在于表单正常提交时也会触发这个事件,用户会看到一个多余的“确定要离开吗”的确认框。解决办法是给表单设置一个“已提交标记”。

let formDirty = false; let formSubmitted = false; document.querySelector('#noteForm').addEventListener('input', function () { formDirty = true; }); document.querySelector('#noteForm').addEventListener('submit', function () { formSubmitted = true; }); window.addEventListener('beforeunload', function (event) { if (formSubmitted || !formDirty) { return; } event.preventDefault(); event.returnValue = ''; });

formDirty在有输入事件时置为 true,表示内容被修改过。formSubmitted在 submit 事件里置为 true,表示用户已经主动提交保存。beforeunload里如果发现已经提交,直接 return 不弹窗。这里有个细节:event.returnValue = ''是一个历史遗留的标准写法,Chrome 和 Firefox 都要求设置该属性才会弹窗,只调用preventDefault()在某些版本上无效。在个人信息展示页面里,点击“编辑”按钮是 location.href 跳转,同样会触发beforeunload,如果这篇笔记本身没有改动,formDirty为 false,不会打扰用户。

4.3 JSP 页面里的集合为 null 时页面 500 的排查思路

共享笔记系统中,sharedUserList这类从 Servlet 传入的数据,在 DAO 层可能返回 null 而不是空集合。比如某篇笔记还没有共享给任何人,listSharedUsers返回了 null,JSP 里用<c:forEach items="${sharedUserList}">遍历时,JSTL 会抛javax.servlet.jsp.JspException导致整个页面 500。这个问题在共享列表为空时必现,属于开发阶段不查数据就发现不了的典型坑。

修复习惯是双保险:DAO 层尽量返回Collections.emptyList()而不是 null;JSP 页面上用empty判断做一个兜底。

<c:choose> <c:when test="${empty sharedUserList}"> <p class="no-data">还没有共享给任何人,点击右上角“共享”按钮添加协作者。</p> </c:when> <c:otherwise> <ul> <c:forEach items="${sharedUserList}" var="su"> <li>${su.username}(${su.permission})</li> </c:forEach> </ul> </c:otherwise> </c:choose>

${empty sharedUserList}对 null 和空列表都返回 true,所以页面逻辑在两种情况下都会走到“暂无数据”分支。使用<c:choose><c:if>更适合这种二选一场景,避免在标签里写反向条件。这类问题在 Tomcat 控制台通常只会显示一行javax.servlet.jsp.JspException: /sharedList.jsp(12,12), 打开对应 JSP 源码第 12 行的 items 属性,排查对应 request 属性名是否拼写正确、Servlet 是否真的执行了setAttribute,这两处不一致是最高发的原因,而不是 JSTL 本身的问题。

5. 用只读权限过滤器验证共享边界,沉淀成可复测的步骤

共享笔记的权限控制如果只靠 JSP 里隐藏按钮,相当于只做了“视觉蒙层”,别人直接 POST 请求<form action="/note/edit">就能绕过。需要把权限判断集中在过滤器或 Serlvet 入口,让 JSP 页面只管展示,权限校验统一在服务端做。下面这个PermissionFilter拦截所有/note/edit/note/delete请求,先判断当前用户是不是笔记作者,不是再看 share 表是否有 write 权限,两者都不满足就返回 403。

@WebFilter("/note/*") public class PermissionFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(false); Integer loginUserId = (session == null) ? null : (Integer) session.getAttribute("userId"); if (loginUserId == null) { response.sendRedirect("/login.jsp"); return; } String uri = request.getRequestURI(); if (uri.endsWith("/edit") || uri.endsWith("/delete")) { String noteIdParam = request.getParameter("id"); if (noteIdParam == null) { response.sendError(400); return; } int noteId = Integer.parseInt(noteIdParam); NoteDAO noteDAO = new NoteDAO(); Note note = noteDAO.findById(noteId); if (note == null) { response.sendError(404); return; } boolean isOwner = note.getUserId() == loginUserId; boolean canWrite = false; if (!isOwner) { ShareDAO shareDAO = new ShareDAO(); canWrite = shareDAO.hasPermission(noteId, loginUserId, "write"); } if (!isOwner && !canWrite) { response.sendError(403, "无编辑权限"); return; } } chain.doFilter(request, response); } }

hasPermission的 SQL 要注意 expire 条件:WHERE note_id=? AND target_user_id=? AND permission='write' AND (expires_at IS NULL OR expires_at > NOW()),这样过期的写权限不会被放行。过滤器里用了request.getSession(false),第二个参数 false 表示 session 不存在时不自动创建,避免静态资源访问也生成一堆无用 session。

验证这套权限是否生效,按照下面的操作顺序走一遍即可:以笔记作者身份登录,创建一篇笔记并共享给 test2 用户,权限选择“只读”;用普通浏览器(未登录状态)打开共享链接/share/view?token=token,必须能看到笔记内容且没有编辑按钮;再用 test2 账号登录系统,直接访问/note/edit?id=该笔记ID,预期返回 403 状态码;回到作者账号,把共享权限改为“可编辑”,test2 再次访问/note/edit?id=该ID,预期能正常加载编辑表单。最后用 Navicat 或命令行执行UPDATE share SET expires_at = '2020-01-01 00:00:00' WHERE note_id=该ID;,刷新共享链接页面,确认返回“共享链接已过期”,然后把过期时间改回未来时间,链接立刻恢复可用。这套流程能在答辩现场用两个浏览器加一条 SQL 完整演示权限模型,比空讲设计图更有说服力。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 2:28:32

MCP3901A0-E/ML不是24位ADC,而是精密模拟前端系统

1. 项目概述&#xff1a;为什么说“MCP3901A0-E/ML”不能只看分辨率&#xff1f;你手头拿到一颗标着“MCP3901A0-E/ML”的芯片&#xff0c;数据手册第一页赫然写着“24-bit delta-sigma ADC with integrated PGA and reference”&#xff0c;旁边还印着“0.1% gain error, 15 p…

作者头像 李华
网站建设 2026/9/14 2:28:15

Python字符串处理与编码优化实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 2:26:47

同伦延拓破解静磁场非线性仿真收敛难题

做静磁场仿真&#xff0c;尤其是变压器、电机这类带铁磁材料的模型&#xff0c;碰到非线性迭代不收敛几乎是家常便饭。很多人以为把B-H曲线和网格都给对了&#xff0c;剩下的就交给求解器&#xff0c;结果牛顿迭代在半路发疯&#xff0c;要么震荡要么直接溢出。这次分享的是我基…

作者头像 李华
网站建设 2026/9/14 2:25:51

纯前端三件套打造拟人化美食推荐页

简介&#xff1a;这是一份面向前端初学者与网页设计爱好者的交互式美食推荐页面实战源码&#xff0c;基于纯HTML/CSS/JavaScript技术栈构建&#xff0c;无需后端即可运行&#xff0c;适用于课程设计、毕业项目或个人作品集开发。资源共191个文件&#xff0c;含23个结构清晰的HT…

作者头像 李华
网站建设 2026/9/14 2:25:29

LLM网关流式内容安全实战:如何让敏感词拦截不中断SSE

1. 项目背景与核心痛点&#xff1a;为什么一个网关要花两周时间反复推倒重来&#xff1f;最近三个月&#xff0c;我接手了公司内部大模型服务的统一接入层重构任务。表面看就是搭个“LLM网关”——把散落在不同部门的模型调用&#xff08;OpenAI、Qwen、GLM、本地部署的Llama3&…

作者头像 李华