
简介一份面向计算机专业毕业设计的网上投稿系统完整设计方案主要解决传统邮寄与电子邮件投稿流程冗长、反馈慢、易丢失等问题系统基于Jsp技术采用B/S架构以SQL Server为后台数据库实现稿件在线提交、审批、编辑与发布一体化管理。文档以河北农业大学本科毕业论文为例包含摘要、关键词、目录、绪论、系统分析、系统设计等章节不仅给出系统总体功能结构还说明了稿件浏览、编辑、添加、更新、删除等模块的设计思路以及前台用户在线写稿、预览、投递和后台管理员审批反馈的完整流程适合需要完成类似课题或参考论文结构的学生使用。整个设计文档只有1个doc格式文件压缩包大小367KB内容集中且层次清晰便于阅读和二次修改目前已有92人学习说明该课题在毕业设计中具备一定参考价值。通过学习这份设计文档读者可快速掌握网上投稿系统的模块划分、数据库表设计要点及关键流程实现对完成毕业设计或课程设计有实际帮助。1. 基于JSP的网上投稿系统从需求拆解到专家审稿闭环如果你正在做毕业设计或者接手了一个“网上投稿系统”的老旧项目维护你会发现这类系统最核心的价值并不在“投稿”本身而在于“专家审稿”这条业务链。一个完整的投稿系统必须同时服务四类角色作者、编辑、审稿专家、系统管理员。作者要能上传稿件、查看进度专家要能在线审稿、填写评审意见编辑要能分配稿件、汇总结果、发出录用通知。很多同学在开题时只关注“能不能传文件”“能不能显示列表”结果答辩时被问到“审稿流程的状态机怎么设计”“专家和编辑的权限怎么隔离”就卡住了。本篇文章我基于标题《基于Jsp的网上投稿系统设计与实现(专家审稿)》梳理一套可以直接照着做的技术方案从数据表设计、JSP Servlet JDBC的分层实现到专家审稿的状态流转、文件上传的避坑指南再到部署到Tomcat的完整步骤全部落成可复现的代码和配置。这篇文章适合正在做JavaWeb课程设计或毕业设计的同学也适合需要快速搭一个内部投稿工具的团队参考——目标是让你拿到标题后能直接动手写代码而不是对着空项目发呆。2. 网上投稿系统的业务模型与数据表设计先定状态机再写代码2.1 四角色权限模型作者、编辑、专家、管理员怎么分投稿系统里最容易被做成一坨浆糊的就是权限模型。很多初学JSP的同学在Session里存一个userType字段然后在每个JSP页面里用if判断显示什么按钮这种做法在小demo里能跑但一旦业务复杂起来页面逻辑会被权限判断塞满后期改一处权限就要翻遍所有页面。我常用的做法是——在Servlet里统一做权限拦截JSP页面不做权限判断。具体来说在web.xml里配置一个Filter根据Session中的role字段判断请求是否允许访问某个URL前缀。比如/author/*只允许作者访问/reviewer/*只允许专家访问/editor/*只允许编辑访问/admin/*只允许管理员访问。这样权限模型清晰而且在答辩时你可以直接说“我采用了过滤器实现基于角色的访问控制”这个技术点本身就值不少分。另一个容易漏掉的细节是角色和用户的关系。一个真实系统里一个人可能既是作者又是专家所以要设计用户表、角色表、用户角色关联表三张表而不是在用户表里加一个role字段了事。虽然课程设计规模不大但三表设计能体现出你对数据库规范化的理解而且后续扩展时不用改表结构。2.2 核心表结构稿件表、审稿表、意见表的关系基础的用户表tb_user至少要包含这些字段user_id主键自增、username、password建议存MD5或SHA-256、real_name、email、phone、role_id外键关联角色表。如果想做得更完善可以加上institution单位/学校和research_direction研究方向这两个字段在专家分配时特别重要。稿件表tb_paper是整个系统的核心我建议这样设计字段CREATE TABLE tb_paper ( paper_id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, abstract_text TEXT, keywords VARCHAR(100), file_path VARCHAR(255) NOT NULL, -- 稿件文件存储路径 author_id INT NOT NULL, -- 投稿作者ID status TINYINT DEFAULT 0, -- 状态0-待分配 1-审稿中 2-审稿完成 submit_time DATETIME, update_time DATETIME, FOREIGN KEY (author_id) REFERENCES tb_user(user_id) );注意这里我把摘要字段命名为abstract_text而不是abstract因为abstract是MySQL的保留字直接用会报错。这是常见的踩坑点很多人在写SQL时没注意建表时直接报语法错误。审稿表tb_review是连接稿件和专家的关联表CREATE TABLE tb_review ( review_id INT PRIMARY KEY AUTO_INCREMENT, paper_id INT NOT NULL, reviewer_id INT NOT NULL, review_status TINYINT DEFAULT 0, -- 0-未开始 1-已提交意见 score DECIMAL(3,1), -- 评分如85.5 comment_text TEXT, -- 评审意见 assign_time DATETIME, finish_time DATETIME, FOREIGN KEY (paper_id) REFERENCES tb_paper(paper_id), FOREIGN KEY (reviewer_id) REFERENCES tb_user(user_id) );为什么要单独做一张tb_review表而不是直接把审稿意见字段塞进tb_paper因为一篇论文通常要分配给2-3个专家进行盲审每个专家都有自己的评分和意见。如果存到稿件表里你只能存一个专家的结果建关联表后一条稿件对应多条审稿记录才能实现真正意义上的“专家审稿”。在答辩时这张表的设计思路会让评委看到你对一对多关系的理解。2.3 状态机设计投稿、分配、审稿、结果发布的流转状态机是整个系统的灵魂也是答辩时容易被追问的地方。我按下述状态来设计0 - 待分配作者投稿成功后的初始状态此时编辑可以在后台看到待分配稿件列表1 - 审稿中编辑为该稿件分配至少一名专家后状态变为审稿中2 - 审稿完成所有被分配的专家都提交了意见状态自动变为审稿完成3 - 已发布结果编辑根据专家意见做出“录用”“修改后录用”“退稿”的决定并且作者能看到结果这个状态流转的校验逻辑放在业务层Service层不要放在JSP页面里。常见做法是作者上传稿件时status置为0编辑执行分配操作时向tb_review表插入记录同时更新tb_paper.status 1当某稿件对应的所有tb_review的review_status都为1时将稿件状态改为2编辑点“发布结论”按钮后状态改为3。判断“所有专家都已审完”的SQL这样写UPDATE tb_paper p SET p.status 2 WHERE p.paper_id ? AND NOT EXISTS ( SELECT 1 FROM tb_review r WHERE r.paper_id p.paper_id AND r.review_status 0 );这句SQL的含义是只有当不存在“已分配但未提交意见”的审稿记录时才把稿件置为“审稿完成”。用一句话就能完成状态流转判断比在Java里循环遍历审稿记录再判定的方式简洁得多而且答辩时展示这句SQL能明显展示你的SQL功底。3. JSP Servlet JDBC三层架构实现从登录到投稿的完整流程3.1 项目结构怎么搭Model / View / Controller 与包命名规范先强调一个原则JSP只负责显示数据Servlet只负责接收请求和跳转页面业务逻辑和数据库操作放在JavaBeanModel里。这虽然是最基础的MVC但很多同学会把% ... %脚本片段写满整个JSP让项目变成一锅粥。下面的结构是经过实际验证、适合课程设计的一版src/ ├── com.example.paper.dao // 数据访问层UserDao, PaperDao, ReviewDao ├── com.example.paper.entity // 实体类User, Paper, Review ├── com.example.paper.service // 业务逻辑层UserService, PaperService, ReviewService ├── com.example.paper.servlet // Servlet控制器LoginServlet, UploadServlet, AssignServlet └── com.example.paper.filter // 权限过滤器LoginFilter, RoleFilter web/ ├── WEB-INF/ │ ├── web.xml │ └── lib/ ├── css/ ├── js/ ├── author/ // 作者页面submit.jsp, myPapers.jsp, paperDetail.jsp ├── reviewer/ // 专家页面reviewList.jsp, reviewForm.jsp ├── editor/ // 编辑页面assign.jsp, reviewResults.jsp ├── admin/ // 管理员页面userManage.jsp └── login.jsp, index.jsp实体类Entity的作用是封装一行数据属性与数据库字段一一对应。例如Paper实体包含paperId、title、abstractText、filePath、authorId、status属性并提供对应的getter/setter。DAO层则通过JDBC把数据库中的行映射成Java对象每个DAO类通常对应一张表——这是最基础也最容易理解的ORM思路。3.2 登录模块与Session管理把用户角色存进去登录模块是所有功能的前提。实现逻辑很简单用户提交用户名和密码到LoginServletServlet调用UserDao.findByUsernameAndPassword()方法查询数据库如果查到记录就把user对象和role对象放进Session然后根据角色跳转到不同的首页。下面是LoginServlet的核心代码WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); // 使用MD5加密密码后再与数据库比对 String hashedPwd MD5Util.getMD5(password); User user userService.login(username, hashedPwd); if (user null) { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } // 关键把用户ID和角色ID存入Session HttpSession session request.getSession(); session.setAttribute(userId, user.getUserId()); session.setAttribute(roleId, user.getRoleId()); session.setAttribute(username, user.getUsername()); // 按角色跳转 if (user.getRoleId() 1) { response.sendRedirect(request.getContextPath() /author/index.jsp); } else if (user.getRoleId() 2) { response.sendRedirect(request.getContextPath() /editor/index.jsp); } else { response.sendRedirect(request.getContextPath() /reviewer/index.jsp); } } }这里有两个细节值得说明。第一密码不能明文存储我用了MD5加盐固定盐值或用户名作为盐均可。虽然现在业界推荐BCrypt但在JSP课程设计这个范围内MD5已经足够体现安全意识而且在答辩时可以回答“为什么不用明文密码”这个问题。第二response.sendRedirect()跳转时没有把用户信息放到request里因为重定向是两次请求request里的数据在第一次请求结束后就消失了。这也是很多新手容易踩的地方——第一版写的request.setAttribute(user, user)再重定向结果目标页面永远拿不到数据。3.3 作者投稿与文件上传Commons-FileUpload组件配置投稿上传是整个系统里技术含量最高的一个点。原生Servlet处理文件上传如果你用getParameter()去拿表单里的普通字段会发现返回null——因为表单一旦设置enctypemultipart/form-data所有表单数据都会变成二进制流必须用专门的组件解析。我一般建议用Apache Commons-FileUpload组件。下载commons-fileupload-1.4.jar和commons-io-2.11.0.jar放进WEB-INF/lib目录后编写UploadServletWebServlet(/upload) public class UploadServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 检查请求是否为文件上传类型 boolean isMultipart ServletFileUpload.isMultipartContent(request); if (!isMultipart) { request.setAttribute(errorMsg, 表单类型错误); request.getRequestDispatcher(/author/submit.jsp).forward(request, response); return; } // 2. 创建解析器 DiskFileItemFactory factory new DiskFileItemFactory(); ServletContext servletContext getServletContext(); File repository (File) servletContext.getAttribute(javax.servlet.context.tempdir); factory.setRepository(repository); ServletFileUpload upload new ServletFileUpload(factory); upload.setFileSizeMax(10 * 1024 * 1024); // 单个文件最大10MB upload.setSizeMax(20 * 1024 * 1024); // 整个请求最大20MB String title null; String abstractText null; String keywords null; String fileName null; try { // 3. 解析请求 ListFileItem items upload.parseRequest(request); for (FileItem item : items) { if (item.isFormField()) { // 普通表单字段用getString()取 String fieldName item.getFieldName(); String value item.getString(UTF-8); if (title.equals(fieldName)) { title value; } else if (abstract.equals(fieldName)) { abstractText value; } else if (keywords.equals(fieldName)) { keywords value; } } else { // 文件字段 fileName new File(item.getName()).getName(); // 只取文件名防路径穿越 String uploadPath getServletContext().getRealPath(/uploads); File uploadDir new File(uploadPath); if (!uploadDir.exists()) { uploadDir.mkdirs(); } File uploadedFile new File(uploadPath, fileName); item.write(uploadedFile); } } // 4. 把稿件信息写入数据库 Paper paper new Paper(); paper.setTitle(title); paper.setAbstractText(abstractText); paper.setKeywords(keywords); paper.setFilePath(/uploads/ fileName); paper.setAuthorId((Integer) request.getSession().getAttribute(userId)); paper.setStatus(0); paperService.addPaper(paper); response.sendRedirect(request.getContextPath() /author/myPapers.jsp); } catch (Exception e) { e.printStackTrace(); request.setAttribute(errorMsg, 上传失败 e.getMessage()); request.getRequestDispatcher(/author/submit.jsp).forward(request, response); } } }代码里最需要警惕的坑在于中文文件名和重名覆盖问题。Tomcat 8.5默认URIEncoding为UTF-8但Commons-FileUpload组件解析文件名时如果不指定编码中文文件名在部分Windows环境会乱码所以调item.getString(UTF-8)是必须的。至于重名更好的方案是用UUID或时间戳重命名文件而不是直接用原文件名保存。3.4 专家审稿与意见提交事务处理和状态更新专家审稿的页面逻辑相对简单专家登录后看到分配给自己的待审稿件列表点击进入详情页在线填写评分和评审意见提交后把意见存入tb_review表同时更新review_status 1。这里有一个值得注意的事务问题如果tb_review表里只有一条审稿记录即只分配了一个专家那么专家提交意见时要同时做两件事——更新review_status以及判断稿件是否进入“审稿完成”状态。这两步操作必须在同一个事务里完成否则可能出现“专家提交了意见但稿件状态还是审稿中”的数据不一致问题。典型代码如下public void submitReview(int reviewId, double score, String comment) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 ReviewDao reviewDao new ReviewDao(); reviewDao.updateReview(conn, reviewId, score, comment); // 更新审稿记录 // 查询同一稿件的其他审稿记录是否都已完成 int paperId reviewDao.getPaperIdByReviewId(conn, reviewId); boolean allDone reviewDao.isAllReviewDone(conn, paperId); if (allDone) { PaperDao paperDao new PaperDao(); paperDao.updatePaperStatus(conn, paperId, 2); // 稿件状态变为审稿完成 } conn.commit(); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw new RuntimeException(审稿提交失败); } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这套写法能应对“单专家审稿”或“多专家审稿”两种情况并且通过isAllReviewDone()方法保证状态流转的原子性。为了拿到connectionDBUtil采用ThreadLocal或每次新建连接都无所谓但setAutoCommit(false)这一步不能省。4. 部署到Tomcat从打War包到配置数据库连接池4.1 环境准备与JDBC连接配置MyEclipse / IDEA 导入项目步骤如果你拿到的是一个.doc格式的毕业设计说明文档大概率还需要自己动手把代码从零建起来。通常需要下列工具版本JDK 1.8、Tomcat 8.5/9.0、MySQL 5.7或8.0。注意JDK版本不要高于1.8——很多旧项目里的JSP页面依赖老版本Tomcat的编译器特性JDK 11或17直接跑老项目会出现各种莫名其妙的编译错误。数据库连接配置建议统一放到src/db.properties文件里而不是把URL、用户名、密码散落在每个DAO类的代码中jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/submission_system?useUnicodetruecharacterEncodingUTF-8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.passwordyourpasswordMySQL 8.0以上必须用com.mysql.cj.jdbc.Driver而且URL里要带上serverTimezone参数否则时间字段会报错。这个坑我在帮人排查时见过至少三次驱动名写错或者时区未设置启动后一操作数据库就抛异常。4.2 Tomcat目录与War包热部署发布到本地的两种方式将项目部署到Tomcat有两种常见方式这里都说一遍。第一种是把项目放到webapps目录下第二种是改conf/server.xml里的Host节点或用IDEA内置的Deployment配置。对课程设计而言我推荐最简单的一种做法——直接把打好的War包拷到webapps下不需要改任何配置。如果你已在IDE里建好Dynamic Web Project导出War包的路径是右键项目 → Export → WAR File。然后把submission_system.war放到Tomcat的webapps目录下启动Tomcat后它会自动解压。访问地址就是http://localhost:8080/submission_system/。为了让项目在启动后自动建表可以在web.xml里配置一个ServletContextListenerpublic class InitListener implements ServletContextListener { public void contextInitialized(ServletContextEvent sce) { // 检查数据库表是否存在不存在则执行建表SQL DBUtil.initTables(); } }这样评审老师拿到你的项目后不用手动打开Navicat执行SQL脚本把项目一扔进Tomcat就能跑这种细节往往能直接影响验收评价。4.3 数据库中文乱码过滤器、连接参数与建表三处排查乱码问题几乎家家都遇到过排查顺序很重要。第一查JSP页面编码确保每个页面都在第一行写上% page contentTypetext/html;charsetUTF-8 languagejava %并且meta charsetUTF-8。第二查数据库连接URL里的characterEncodingUTF-8第三查建表时表的字符集。建表语句末尾最好显式指定字符集CREATE TABLE tb_paper (...) ENGINEInnoDB DEFAULT CHARSETutf8mb4;使用utf8mb4而不是utf8是因为utf8在MySQL里最多只能存3字节的字符表情符号存不进去。如果你的系统不涉及emojiutf8也够用但utf8mb4是更稳妥的选择。光有页面和连接配置还不够最好加一个全局编码过滤器统一处理请求乱码WebFilter(/*) public class CharacterEncodingFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); chain.doFilter(request, response); } }注意这个过滤器要放在其他过滤器如权限过滤器之前因为后面的业务代码在读取request.getParameter()时使用的编码已经由这个过滤器定好了。5. 避坑指南网上投稿系统开发中常见的五个拦路虎5.1 文件上传后访问404路径到底写没写对现象文件确实上传到了服务器磁盘数据库里的file_path看起来也写进去了但浏览器访问http://localhost:8080/submission_system/uploads/xxx.pdf时返回404。原因Tomcat默认的Web应用上下文根目录是webapps/submission_system/getServletContext().getRealPath(/uploads)获取的其实是你项目部署后在Tomcat下的物理路径。如果你是用IDE部署的这个路径通常是target/classes或某个临时工作目录并不是你以为的项目根目录。更关键的是如果你上传到了WEB-INF目录下浏览器天然禁止访问如果上传到项目根目录但IDE没同步刷新也会出现404。解决不要使用getRealPath()这种依赖部署形态的写法建议把上传路径配置成绝对路径。比如在db.properties同级放一个config.propertiesupload.dirD:/upload_files/上传时直接写到D:/upload_files/下载时直接用FileInputStream读取磁盘文件并响应到浏览器。这样无论项目部署在Tomcat的哪个位置文件都不会丢。如果要下载文件用下面这段代码WebServlet(/download) public class DownloadServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String filePath request.getParameter(filePath); File file new File(filePath); if (!file.exists()) { response.sendError(404, 文件不存在); return; } response.setContentType(application/octet-stream); response.setHeader(Content-Disposition, attachment;filename URLEncoder.encode(file.getName(), UTF-8)); FileInputStream fis new FileInputStream(file); OutputStream os response.getOutputStream(); byte[] buffer new byte[1024]; int len; while ((len fis.read(buffer)) ! -1) { os.write(buffer, 0, len); } fis.close(); os.close(); } }5.2 专家重复审稿数据库里出现两条审稿结果现象同一个专家对同一篇论文提交了两次意见数据库里出现两条review记录而且第二次提交把第一次覆盖或新增了。原因没有在tb_review表上做唯一约束。在投稿系统中一篇稿件分配给同一专家只能有一条审稿记录这是业务规则。解决建表时给(paper_id, reviewer_id)加上联合唯一索引ALTER TABLE tb_review ADD UNIQUE INDEX uk_paper_reviewer (paper_id, reviewer_id);同时在ReviewDao里插入前先判断是否已存在记录存在则执行UPDATE不存在才执行INSERT。双管齐下才能从根本上杜绝重复审稿。5.3 文件上传大小超过默认限制直接抛异常现象明明表单里写了enctypemultipart/form-data但上传超过1MB的文件时Tomcat直接抛MaxUploadSizeExceededException页面卡死。原因你没有在ServletFileUpload上设置setSizeMax()而且Tomcat的默认请求体大小限制也会拦截。老版Tomcat默认单请求体上限是2MB超过了就报错。解决设置上传解析上限代码见3.3节同时在Tomcat的conf/server.xml里找到Connector节点增加属性Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 maxPostSize20971520 /maxPostSize的单位是字节20MB大约对应20971520。设置后重启Tomcat才生效。5.4 JSP页面直接访问Servlet跳转路径迷失base标签的妙用现象在/author/submit.jsp页面里写a href/submit投稿/a点了之后浏览器地址栏变成localhost:8080/submit而不是localhost:8080/submission_system/submit页面404。原因以/开头的URL是绝对路径它会直接跳到当前域名和端口的根路径不会带上项目上下文路径即/submission_system这个前缀。解决在JSP页面的head区里加一个base标签% String basePath request.getScheme() :// request.getServerName() : request.getServerPort() request.getContextPath() /; % base href%basePath%加了这行之后页面里所有相对路径都会自动以项目根路径/作为基准不会再出现路径缺失或多一级的情况。5.5 评审结果是“可修改后录用”但作者找不到修改入口现象系统实现了“专家审稿”状态流转但作者登录后只能看到稿件状态是“审稿完成”没有重新提交修改稿的功能入口。原因状态机设计少了“返修”分支。在实际的业务闭环里编辑看完专家意见后决定“退稿”或“录用”都好办——一个状态更新就完事。但“修改后重投”就需要第二次投稿流程很多课程设计为了省事会砍掉这个分支结果答辩时被评委一句话问住“专家建议修改你怎么走流程”解决在tb_paper表再加一个字段is_revision或者复用status状态码4代表“退修中”。作者上传修改稿时不新增一条稿件记录而是更新原记录的文件路径和状态并把状态变成1重新进入审稿。这样流程完整代码改动也少。专家审稿系统做到这一步不论功能上还是答辩效果上都会提升一个档次。6. 进阶技巧用邮件通知驱动审稿进度而不是让用户反复刷新如果你还想在答辩时多几个亮点我建议加一个“邮件通知”模块。现在的系统里专家是否知道自己有新的审稿任务作者是否知道稿件已经分配完成如果都不通知用户只能反复登录系统去刷新状态。加邮件通知后业务体验立刻不一样。实现思路不难——用JavaMail发送邮件。在专家被分配稿件时调用邮件工具类给他的邮箱发一封邮件作者上传稿件时给编辑发一封提醒邮件。大致步骤下载javax.mail.jar或jakarta.mail.jar取决于JDK版本放到WEB-INF/lib。写一个邮件工具类MailUtil配置SMTP服务器参数。在AssignServlet分配专家成功后调用MailUtil.sendReviewNotification()。核心代码public class MailUtil { // 使用163或QQ邮箱SMTP服务发送邮件 private static final String SMTP_HOST smtp.163.com; private static final String SMTP_PORT 465; private static final String USERNAME your_email163.com; private static final String PASSWORD your_smtp_auth_code; public static void send(String to, String subject, String content) throws MessagingException { Properties props new Properties(); props.put(mail.smtp.host, SMTP_HOST); props.put(mail.smtp.port, SMTP_PORT); props.put(mail.smtp.auth, true); props.put(mail.smtp.ssl.enable, true); Session session Session.getInstance(props, new Authenticator() { Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(USERNAME, PASSWORD); } }); MimeMessage message new MimeMessage(session); message.setFrom(new InternetAddress(USERNAME)); message.setRecipient(Message.RecipientType.TO, new InternetAddress(to)); message.setSubject(subject, UTF-8); message.setText(content, UTF-8); Transport.send(message); } }注意这里的PASSWORD不是你邮箱的登录密码而是SMTP授权码需要在邮箱设置里单独开启。我用163邮箱时第一次写死登录密码发送结果报535 Error: authentication failed排查了好半天才发现是授权码和密码的区别。另一个容易忽略的问题是异步发送。千万别在Servlet里直接同步调用MailUtil.send()因为SMTP握手和发送过程通常需要1-3秒用户点击“分配专家”按钮后会白白等好几秒。常见做法是开一个线程池ExecutorService executor Executors.newFixedThreadPool(5); executor.submit(() - { try { MailUtil.send(to, subject, content); } catch (MessagingException e) { e.printStackTrace(); } });这样用户体验会好很多也体现出你有并发编程意识。在答辩中被问到“系统有哪些优化点”时你可以说异步通知、连接池复用、上传文件用时间戳重命名等这些都是加分项。最后我想说网上投稿系统这类JavaWeb设计的难点从来不是CRUD本身而是“业务闭环是否完整”与“异常情况是否有兜底”。你多想一想“专家没提交意见怎么办”“上传超大文件怎么办”“两个专家意见相反怎么办”这些问题就能比同期同学多迈出一步。我在做这套系统的过程中踩过的坑基本都写在这篇文章里了——希望你拿到手后能把它们提前绕开把时间花在真正有价值的状态机设计和流程体验上。希望帮到你。本文还有配套的精品资源点击获取