news 2026/10/5 1:16:36

JavaWeb作业管理网站源码解析:从部署到改造避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaWeb作业管理网站源码解析:从部署到改造避坑指南

简介:面向初学JavaWeb的开发者,这套作业管理网站项目源码覆盖教师发布作业、学生提交作业与管理员后台管理三大核心场景,适合课程设计、毕业设计及Servlet/JSP技术实战练习。压缩包共53个文件、约7.73MB,以JSP页面、Java类、Jar依赖库为主,辅以CSS样式、JS脚本和数据库配置信息,目录结构清晰,便于直接导入开发工具运行。目前已有97人学习。资源内含登录与信息维护、作业发布、作业汇总、作业提交等完整功能页面,涉及Servlet请求处理、JDBC数据交互、MVC分层设计以及Bootstrap前端框架,可帮助读者掌握作业管理系统的前后端联动、权限校验与数据库操作实现思路。

1. JavaWeb作业管理网站:这套源码包里到底有什么可以复用的

做JavaWeb课程设计或者接手老项目时,最怕的不是业务复杂,而是拿到一个结构不明的zip包,解压后不知道从哪行代码开始读。这份基于JavaWeb的作业管理网站源码,解压后是一个典型的Servlet + JSP + JDBC三层结构,没有引入Spring那套重量级依赖,数据源用的是druid.properties配置的Druid连接池,正好适合拿来理解JavaWeb最原始的请求-处理-响应链路。如果你正打算做一套「教师发作业、学生交作业、管理员管数据」的小系统,或者想把课设里的SSM项目降级成纯JSP/Servlet版本以便讲清楚原理,这份源码是一个可以快速跑通的参考底座。它能帮你省下搭骨架的时间,直接看到登录、发布作业、提交作业、统计收集情况这几条主流程在JSP页面和Servlet之间是怎么流转的。

2. 先拆运行链路:Servlet、JSP、JDBC在作业流转里各自扮演什么角色

2.1 从index.jsp到login.jsp:请求入口与页面跳转的设计习惯

打开压缩包先看页面文件。index.jsp通常是站点入口,常见做法是放一个重定向或者欢迎页,根据session里有没有登录用户决定是进login.jsp还是直接进work.jsp。这份源码里login.jsp被单独拆出来,说明登录逻辑是独立处理的。

<!-- index.jsp --> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% // 检查session中是否有已登录用户 Object user = session.getAttribute("user"); if (user != null) { // 已登录:直接进入作业列表页 response.sendRedirect("work.jsp"); } else { // 未登录:先去登录页 response.sendRedirect("login.jsp"); } %>

这里的核心逻辑是:不要在前端页面里写死跳转地址,而是通过session状态做一次判断。很多新手课设会把index.jsp直接做成登录表单页,但实际项目中入口页往往是路由分发的作用。参数方面,session.getAttribute("user")里存的user对象是在登录Servlet里set进去的,这个命名要保持全项目一致,否则跳到work.jsp后拿不到用户信息,页面就会报空指针。

JSP页面本身不做业务判断,它只负责根据已有数据渲染界面。所以work.jsp、gather.jsp、issue.jsp这些页面里,你会发现大量的<% %>脚本片段和<%= %>输出标签,它们拿到的数据都是Servlet通过request.setAttribute()塞进来的。理解这一点,你就能快速定位每张页面对应的Servlet是哪个。

2.2 druid.properties与JDBC:数据库连接池配置的四个关键参数

源码里的druid.properties是整站数据访问的命根子。JavaWeb项目里直接写Class.forName("com.mysql.jdbc.Driver")硬编码连接的做法已经过时了,用Druid连接池管理数据库连接是更稳的姿势,尤其在并发提交作业的场景下,连接池能避免频繁创建和销毁连接带来的性能损耗。

# druid.properties driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/homework_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username=root password=123456 initialSize=5 maxActive=10 maxWait=3000

四个关键参数逐个说:driverClassName对应你用的MySQL版本,5.x用com.mysql.jdbc.Driver,8.x要换成com.mysql.cj.jdbc.Driver,不换会直接报ClassNotFoundException。url里的characterEncoding=utf8是必须的,不写的话中文作业标题入库后全是问号。useSSL=false只用于本地开发环境,生产环境建议开启SSL或者放在内网。serverTimezone=Asia/Shanghai是MySQL 8.x的硬性要求,少了它连接会报时区错误。连接池参数里initialSize=5表示启动时预建5个连接,maxActive=10是最大连接数,maxWait=3000是拿不到连接时的最长等待毫秒数,超过就抛异常。做课设的话这组参数够用,但如果你要模拟全班同时交作业,建议把maxActive调大到20,否则高并发下会看到wait millis 3000, active 10, maxActive 10的报错。

2.3 MVC在源码里怎么落地的:Servlet是Controller,JSP是View,那Model在哪

很多人在课设里写MVC,写着写着就变成了JSP里既写SQL又写HTML。这份源码的好处在于它把Controller和View分开了,但Model层用的是最朴素的DAO写法,没有Service层。看web目录下的Servlet,你会发现每个Servlet只做三件事:接收参数、调用DAO、跳转页面。

// LoginServlet.java 核心片段 @WebServlet("/login") public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 接收前端表单参数 String username = request.getParameter("username"); String password = request.getParameter("password"); // 2. 参数校验:空值直接打回 if (username == null || password == null || username.isEmpty() || password.isEmpty()) { request.setAttribute("msg", "用户名和密码不能为空"); request.getRequestDispatcher("login.jsp").forward(request, response); return; } // 3. 调用DAO查询用户 UserDAO dao = new UserDAO(); User user = dao.findByUsernameAndPassword(username, password); // 4. 根据结果跳转 if (user != null) { request.getSession().setAttribute("user", user); response.sendRedirect("work.jsp"); } else { request.setAttribute("msg", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } } }

这段代码的逻辑说明:第一步从request里拿参数,对应前端表单的name属性,名字对不上拿到的就是null;第二步做空值校验,这里有个新手常见坑——只校验了null和空字符串,没有trim掉首尾空格,用户输入" admin "就会查不到;第三步调DAO,DAO里再写JDBC查询;第四步跳转时区分了重定向和转发,登录成功后用sendRedirect是防止刷新页面重复提交表单,登录失败用forward是为了把msg错误信息带到JSP页面上显示。

关于Model层,这份源码的Model是散落在DAO类和实体类里的。你会在com目录下看到User、Homework、Submission这样的实体类,它们对应数据库表结构。这种写法虽然不够"企业级",但好处是链路短,特别适合还没学框架的人理解。如果你想改成Spring MVC版,只需要把@WebServlet("/login")换成@RequestMapping("/login"),把request.getParameter换成@RequestParam,业务逻辑几乎可以原样搬过去。

3. 把项目跑起来:IDEA配置、Tomcat部署与数据库初始化

3.1 前置环境:JDK版本、Tomcat版本、MySQL版本怎么搭配不翻车

先说结论,我试过几组搭配,最稳的是JDK 1.8 + Tomcat 8.5 + MySQL 5.7,如果你机器上装的是MySQL 8.0,那就把pom里的驱动换成8.x版本,同时url里加serverTimezone=Asia/Shanghai。JDK 11以上跑老项目会遇到一个问题:Tomcat 8.5默认用的是JSP 2.3规范,而JDK 11里移除了JSP相关的EE包,你需要在WEB-INF/lib下补一个jsp-api.jar,不然启动时控制台会报java.lang.NoClassDefFoundError: javax/servlet/jsp/JspFactory。

Tomcat版本和JDK的对应关系也容易踩坑。Tomcat 9需要JDK 8以上,但Tomcat 10把javax.servlet改成了jakarta.servlet,这份源码里的import javax.servlet.*在Tomcat 10下直接编译不过。所以如果你用的是Tomcat 10,要么换回Tomcat 9,要么全局替换javax为jakarta,我建议直接换Tomcat 9省事。

MySQL这边注意字符集。建库的时候用CREATE DATABASE homework_db DEFAULT CHARACTER SET utf8mb4;,别用默认的latin1。utf8mb4和utf8的区别在于前者能存emoji表情和生僻字,JavaWeb项目里学生提交作业内容可能带特殊符号,用utf8mb4能少一个乱码坑。数据库连接用户的密码要和druid.properties里的password=123456对齐,不然启动时报Access denied for user 'root'@'localhost'。

3.2 IDEA导入本地zip项目的完整步骤:不要直接Open根目录

很多新手拿到zip解压后直接IDEA Open,结果项目结构识别不出来,全是红色报错。正确姿势是这样的:

  1. 解压zip,确认目录结构里有src和web/WEB-INF,这说明是war包结构,不是Maven结构。
  2. IDEA里选择File -> New -> Project from Existing Sources,选到解压后的根目录。
  3. 弹窗里选Create project from existing sources,一路Next,到Select project name and location那一步,改一下Project name,避免默认名带特殊字符。
  4. 导入完成后,右键项目根目录选Add Framework Support,勾选Web Application,IDEA会自动识别web.xml并配置好Web模块。
  5. 如果源码里有Maven的pom.xml,IDEA会提示Maven项目,点导入即可。如果没有pom.xml,说明用的是纯JSP/Servlet + lib目录下的jar包,检查WEB-INF/lib下有没有mysql-connector.jar和druid.jar,没有的话需要手动添加到Libraries。

配置好之后,还要设置一下项目的JDK级别。File -> Project Structure -> Project里把SDK选成1.8,Modules -> Dependencies里确保lib目录下的jar已经被引入。这一步漏了的话,编译时所有import com.alibaba.druid.*和import com.mysql.*全会飘红。

3.3 配置Tomcat并部署war包:Artifact的坑你躲不开

IDEA里跑JavaWeb项目,本质是启动一个本地Tomcat实例,然后把项目部署上去。很多人卡在Run按钮是灰色,原因是没配Artifact。

# 在IDEA菜单栏 Run -> Edit Configurations -> + -> Tomcat Server -> Local # Server标签页:Application server选择本地Tomcat安装路径 # Deployment标签页:+ -> Artifact -> 选择 xxx:war exploded # Application context填 /,这样访问时不用带项目名

这里有个关键选择:xxx:war和xxx:war exploded的区别。前者是压缩包模式,Tomcat每次启动先解压war;后者是解压目录模式,改动JSP后刷新页面立即生效,不用重新打包。开发阶段一定要选exploded,不然你改一行JSP代码就要重启整个Tomcat,血泪经验。

Application context建议填/,这样浏览器访问http://localhost:8080/index.jsp就到了项目入口。如果填了/homework,那你访问路径得带项目名,JSP里的response.sendRedirect("work.jsp")这类相对路径在跳转时容易出问题——当前路径变成/homework/work.jsp,如果Servlet的注解是@WebServlet("/work"),而JSP里写的是<form action="login">,那提交表单时请求会打到/homework/login,能通,但如果你在Servlet里用了response.sendRedirect("/work.jsp"),带不带项目名就是天壤之别了。统一用/可以少踩一半的路径坑。

3.4 初始化数据库:从源码里的SQL脚本反推表结构

源码zip里不一定自带SQL脚本,这时你需要从实体类和DAO层的代码里反推表结构。我一般会先打开com目录下的实体类,看看有哪些字段,再打开DAO类看查询语句用了哪些列名。

-- 根据源码实体类和DAO反推的核心表结构 CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(50) NOT NULL, role TINYINT DEFAULT 0 COMMENT '0-学生 1-教师 2-管理员', real_name VARCHAR(20) ); CREATE TABLE homework ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL, content TEXT, teacher_id INT, deadline DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (teacher_id) REFERENCES user(id) ); CREATE TABLE submission ( id INT AUTO_INCREMENT PRIMARY KEY, homework_id INT, student_id INT, submit_time DATETIME, file_path VARCHAR(255), status TINYINT DEFAULT 0 COMMENT '0-未提交 1-已提交', FOREIGN KEY (homework_id) REFERENCES homework(id), FOREIGN KEY (student_id) REFERENCES user(id) );

表结构设计里要注意三点。第一,role字段用TINYINT存角色,比用字符串存省空间,但代码里要用常量维护,比如User.ROLE_TEACHER = 1,别在DAO里写魔法数字where role = 1,不然三个月后你自己都看不懂。第二,submission表里的status字段看似冗余,因为存在记录就说明提交过了,但实际业务里存在"教师撤销提交让学生重交"的场景,所以保留状态位更灵活。第三,外键约束在生产环境可以不加,但课设项目里加了能向评委展示你有数据一致性意识。

初始化数据时,至少插入一个教师账号、一个学生账号、一条作业记录。密码建议直接用明文,因为这份源码的登录校验大概率是select * from user where username = ? and password = ?,你如果手贱把密码加密了,反而登录不进去。等后面改造时再统一加MD5或BCrypt。

3.5 启动顺序与验证清单:看到login.jsp只算成功了一半

启动Tomcat后,浏览器输入http://localhost:8080/index.jsp,能跳转到login.jsp说明Tomcat和项目本身没毛病。但这时候别急着欢呼,按下面清单逐项验证:

验证项操作预期结果
登录功能用教师账号登录跳转到work.jsp,显示作业列表
发布作业教师身份进入issue.jsp,填写标题和内容提交跳转到work.jsp后能看到新作业
学生登录退出后用学生账号登录看到教师发布的作业,但不能显示发布按钮
提交作业学生点击提交,输入作业内容submission表新增记录,gather.jsp能看到统计数
权限拦截未登录直接访问work.jsp页面能打开但拿不到数据,或者跳回login.jsp

这里第五项是个大坑。如果源码里的Servlet只在登录时校验密码,没有做session拦截,那未登录用户直接访问work.jsp时,页面里的session.getAttribute("user")是null,如果JSP脚本里直接调用user.getUsername(),Tomcat会抛NullPointerException,页面白屏。验证这个场景很有必要,如果翻车了,有两个补救方案:要么在JSP页面顶部加判断,要么写一个AuthFilter拦截所有*.jsp和Servlet路径。后面第四章我会细说Filter的方案。

4. 避坑与排查:JavaWeb作业管理跑不起来最常见的五个问题

4.1 现象:Tomcat能启动,但访问页面报404

原因:Artifact没部署上去,或者Application context路径不对。IDEA里Tomcat虽然启动了,但Deployment标签下如果没有添加Artifact,Tomcat就是个空壳服务器,访问任何路径都404。另外,如果你改过Application context,比如从/改成了/homework,旧的重定向代码response.sendRedirect("index.jsp")会定位到/homework/index.jsp,但如果JSP里写的又是相对路径<a href="login.jsp">,当前浏览器地址栏的路径决定了这个相对路径解析到哪,混用绝对路径和相对路径就会交叉出404。

解决:检查Run -> Edit Configurations -> Deployment里有没有xxx:war exploded,没有就点加号补上。Application context统一用/,然后所有Servlet的@WebServlet注解用/xxx格式,JSP页面之间跳转用相对路径,Servlet跳JSP用request.getRequestDispatcher("xxx.jsp"),这样整个项目的路径规则是自洽的。

4.2 现象:页面中文全是问号,数据库里存的数据也是乱码

原因:三层乱码,每一层都可能出问题。第一层是JSP页面编码,<%@ page contentType="text/html;charset=UTF-8" %>没写或者写错;第二层是请求编码,Tomcat默认用ISO-8859-1解码POST请求,你Servlet里直接request.getParameter()拿到的中文就是乱码;第三层是数据库连接编码,druid.properties里url少了characterEncoding=utf8。

解决:三层分别处理。JSP页面头部加<%@ page contentType="text/html;charset=UTF-8" language="java" %>。Servlet里在读取任何参数之前加request.setCharacterEncoding("UTF-8")。对于GET请求,还需要在Tomcat的conf/server.xml里给Connector加URIEncoding="UTF-8"属性。数据库url保证有characterEncoding=utf8且物理表结构是utf8mb4。这三层少一层,中文就乱给你看。

4.3 现象:登录时明明输入正确,却提示用户名或密码错误

原因:第一,数据库表里的密码有肉眼不可见的空格,比如建库脚本里写了'123456 '带了个尾随空格;第二,druid.properties里连接的数据库和实际初始化的数据库不是同一个库,你往里插数据的表和程序查的表对不上;第三,登录Servlet里没做trim(),用户输入时手滑带了个空格。

解决:先用SQL客户端直接查select * from user where username = 'admin',确认表里有数据且密码正确。然后在Servlet的DAO层打印日志,Log一下实际执行的SQL和传入的参数。如果SQL没问题,那就是字符集或空格问题,在SQL里用where username = ?配合username.trim()过滤。这一步排查最重要的原则:先用数据库工具排除数据本身的问题,再怀疑代码。

4.4 现象:学生提交作业后,gather.jsp页面的统计数字对不上

原因:统计SQL用了COUNT直接select count(*) from submission,但没有按homework_id分组,所以所有作业的提交数混在一起;或者LEFT JOIN的关联字段写错,比如submission.student_id关联到了homework.teacher_id,统计结果自然全是0。

解决:打开gather相关的Servlet,看一下统计SQL。正确写法应该按作业ID分组:

-- gather.jsp背后的统计逻辑 SELECT h.id, h.title, COUNT(CASE WHEN s.status = 1 THEN 1 END) AS submitted_count, COUNT(u.id) AS total_students FROM homework h LEFT JOIN submission s ON h.id = s.homework_id LEFT JOIN user u ON u.role = 0 GROUP BY h.id, h.title;

这段SQL的细节:LEFT JOIN保证即使没人提交作业,作业记录也能显示在列表里,COUNT(CASE WHEN s.status = 1 THEN 1 END)只统计已提交的学生,COUNT(u.id)统计所有学生总数。如果源码里用的是内连接INNER JOIN,没提交的作业就不会出现在统计页面上,这就是数字对不上的根本原因。

4.5 现象:JSP页面报错java.lang.NullPointerException,定位到session.getAttribute("user")那行

原因:JSP脚本里直接使用了session中不存在的属性。用户未登录、或者session过期了,user变量是null,你直接调用user.getId()就会炸。

解决:JSP页面顶部加判断,更规范的做法是写过滤器统一拦截。我一般会在com包下新建一个AuthFilter类:

// AuthFilter.java 权限拦截过滤器 @WebFilter("/*") public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 放行登录页和登录请求 String path = request.getRequestURI(); if (path.endsWith("login.jsp") || path.endsWith("/login") || path.endsWith("index.jsp")) { chain.doFilter(req, resp); return; } // 检查session if (request.getSession().getAttribute("user") == null) { response.sendRedirect("login.jsp"); return; } chain.doFilter(req, resp); } }

这段代码的逻辑:从request.getRequestURI()拿到当前请求路径,endsWith判断是不是公开页面,是就放行。然后检查session里有没有user,没有就踢回登录页。@WebFilter("/*")表示拦截所有请求,包括静态资源,所以要在放行条件里加上/css/、/js/、/images/这些静态资源路径,否则Bootstrap样式全加载不出来,页面裸奔。

5. 进阶改造:给作业管理系统加上截止时间校验与文件上传

基础版本跑通之后,下一步值得做的改造有两个:一是发布作业时带上截止时间,提交作业时校验是否已过期;二是把提交内容从文本框改成文件上传。这两个改造都能让项目从"课设demo"变成"接近真实可用"。

截止时间校验的核心思路是在submit相关的Servlet里加一道时间判断:

// SubmitHomeworkServlet 核心逻辑 @WebServlet("/submitHomework") public class SubmitHomeworkServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); User user = (User) request.getSession().getAttribute("user"); int homeworkId = Integer.parseInt(request.getParameter("homework_id")); // 检查提交内容 String content = request.getParameter("content"); // 加载作业信息,判断截止时间 HomeworkDAO hdao = new HomeworkDAO(); Homework homework = hdao.findById(homeworkId); Date deadline = homework.getDeadline(); Date now = new Date(); if (deadline != null && now.after(deadline)) { request.setAttribute("msg", "作业已截止,无法提交"); request.getRequestDispatcher("submit.jsp").forward(request, response); return; } // 通过校验后执行提交逻辑 SubmissionDAO sdao = new SubmissionDAO(); boolean success = sdao.submitHomework(homeworkId, user.getId(), content); // 成功跳转回作业列表 response.sendRedirect("work.jsp"); } }

这段代码的核心不是提交动作,而是now.after(deadline)这个时间判断。有一个容易被忽略的点:如果作业没有设置截止时间,deadline可能是null,所以要加空值保护,否则now.after(null)会抛NPE。时间格式的话,前端表单里用<input type="datetime-local">,后端解析时格式化yyyy-MM-dd'T'HH:mm,这个T是HTML5时间控件的标准格式。

文件上传改造需要引入commons-fileupload和commons-io两个jar包。如果你不想引入第三方库,可以用Servlet 3.0原生的@MultipartConfig注解加request.getPart("file")来获取上传文件。这个方案的好处是不用额外依赖,但要确认Tomcat版本支持Servlet 3.0及以上,Tomcat 8.5完全没问题。

// 文件上传核心片段 @MultipartConfig(maxFileSize = 1024 * 1024 * 10, maxRequestSize = 1024 * 1024 * 50) public class UploadServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Part filePart = request.getPart("file"); String fileName = Paths.get(filePart.getSubmittedFileName()).getFileName().toString(); // 避免重名覆盖,拼上时间戳 String newName = System.currentTimeMillis() + "_" + fileName; String uploadPath = getServletContext().getRealPath("/uploads"); File uploadDir = new File(uploadPath); if (!uploadDir.exists()) { uploadDir.mkdirs(); } filePart.write(uploadPath + File.separator + newName); // 把文件路径存到submission表里 SubmissionDAO sdao = new SubmissionDAO(); sdao.submitHomeworkWithFile(user.getId(), homeworkId, "uploads/" + newName); } }

文件上传这里有三个血泪坑。第一个是getSubmittedFileName()在不同浏览器下返回的路径不一样,IE会返回完整的绝对路径,所以要用Paths.get().getFileName()只取文件名部分。第二个是文件名重名问题,两个学生交的文件叫report.docx,先交的会被后交的覆盖,解决方式就是加时间戳或者随机数前缀。第三个是uploadDir.mkdirs(),如果父目录不存在,必须用mkdirs()而不是mkdir(),否则返回false且目录创建不成功。我最初用mkdir()排查了半天,换成mkdirs()立刻就好了。

改造完成后,记得重新验证一下完整链路:教师发布带截止日期的作业 → 学生在截止日期前上传文件 → 截止日期后再次提交被拦截 → gather.jsp统计里能看到提交状态和文件路径。这个流程走通,你的项目从功能完整度上已经超过大部分课设了。从那以后我每次跑JavaWeb项目,都强制自己先走一遍「未登录访问拦截、中文乱码、路径404」这三关,确认没有基础问题再往深了改。这套源码虽然朴素,但正因为没有框架封装,反而逼你把Servlet、JSP、JDBC、Filter这些底层的恩怨情仇全部看清楚,希望帮到你。

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

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

AXI Uartlite IP核详解:FPGA串口调试与Zynq嵌入式应用实践

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

作者头像 李华
网站建设 2026/10/5 1:15:30

MT6835 SPI磁编码器调试实战:STM32读取角度与防抖指南

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

作者头像 李华
网站建设 2026/10/5 1:15:29

高通平台音频控件封装实战:架构设计与调试指南

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

作者头像 李华
网站建设 2026/10/5 1:15:23

STM32C5驱动0.96寸OLED实现3D立方体渲染与性能优化

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

作者头像 李华
网站建设 2026/10/5 1:14:42

从零搞懂机器视觉特征提取:方法、应用与学习路线

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

作者头像 李华