news 2026/10/26 1:06:13

Spring Boot考务管理系统:排考算法、冲突检测与Excel导入实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot考务管理系统:排考算法、冲突检测与Excel导入实战

简介:考务管理系统的设计与实现项目资源,面向高校或培训机构中需要开发考试组织管理软件的开发者,也适合作为毕业设计或Spring Boot前后端分离项目的学习参考。压缩包仅10.36MB,包含456个文件,其中Java后端源码与Vue前端组件构成系统主体,辅以SVG图标、JS逻辑、CSS样式、SQL数据库脚本以及开发文档,结构清晰,便于按模块查阅。已有48人学习。资源完整收录学生信息管理、课程安排、考试安排、成绩录入与查询统计等模块的实现思路,附有数据库表结构说明、源码组织说明和接口设计文档,可帮助理解RESTful API交互方式及系统安全设计;同时提供一键安装、运行与构建脚本,便于快速启动项目并进行二次扩展,是课设毕设与工程实践的有力参考。

1. 考务管理系统不是花瓶:一份代码包背后的业务边界与落地价值

每逢期中期末,教务老师最怕的不是试卷难出,而是面对一张写满考生名单和考场号的Excel表。同一名学生被排进两个教室、监考老师一周被排满五次而另一位老师一次都没轮上、考试通知发下去才发现教室容量不够——这些听着像段子的事,几乎每所学校的考务数据里都发生过。考务管理系统的设计与实现,要解决的正是这条流水线:考试计划下发、学生编排考场、监考教师分配、座位号生成、成绩回执归档。它比一般CRUD项目多出的那部分难度,全压在“排考”和“冲突检测”上。这篇文章适合两类人:拿这类题目做毕业设计的学生,以及想给自己学校搭建内部考务工具的后端开发。我会从技术选型讲到表结构,再给出排考算法、导入脚本和线上排查清单,照着复现即可。

2. 技术选型与表结构设计:为什么默认选 Spring Boot + Vue 而不是单页大杂烩

2.1 前后端分离为什么是这类系统的默认答案

考务系统的使用场景是典型的小团队、多角色、弱并发:教务员建计划,辅导员导入名单,监考老师查场次,学生查座位。最方便维护的方式是前后端分离,后端统一出接口,前端按角色拆页面。基于springboot vue这类组合,在校园管理系统里已经是默认答案——Spring Boot 负责事务和算法,Vue 负责表格和交互,二者通过 JSON 通信,职责清楚。

后端用 Java 的原因不只是生态熟:排考和监考分配涉及大量集合操作、约束判断、事务回滚,Spring Boot 的声明式事务几行注解就能保证数据一致性。前端用 Vue 加 Element Plus,表格组件、日期选择器、弹窗表单都是现成的,学生端只要一个只读页面,连登录都可以复用后端的 RBAC 接口。对比一下备选方案:

方案优点不选它的理由
Spring Boot + Vue事务可靠、生态成熟、招聘量大项目体积稍重,但可接受
Python Flask + 模板渲染快、代码量少多人同时操作的并发与事务控制弱
Node.js + React全栈同一门语言后端算法部分表达不如 Java 直观
PHP 单页部署省事排考这类复杂业务后期改造成本高

真正让我坚持前后端分离的理由是:考务系统后期几乎一定会加移动端查询。学生要看考场、老师要查监考安排,如果后端接口和权限模型一开始就按 REST 设计,后面套一个微信小程序或者手机 H5 不需要动主业务代码。所以这里的技术选型决定的是未来半年的维护成本,不是“够用就行”。

2.2 核心表结构:六张表把考务链路串起来

很多初学者设计的考务系统只有“考试信息表”和“学生表”,这是最典型的翻车现场。考务的核心不是“知道有这场考试”,而是“谁在什么时间、哪个教室、哪个座位、被哪位老师监考”。我一般会拆成六张表:考试计划表、课程表、考生名单表、考场表、考试场次表、安排结果表。下面是一份可以直接执行的 MySQL DDL,字段按最小可用集设计,去掉了冗余的软删除标记。

CREATE TABLE exam_plan ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plan_name VARCHAR(50) NOT NULL COMMENT '计划名称,如2025春期末考试', semester VARCHAR(20) NOT NULL, created_by VARCHAR(30), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_semester_name (semester, plan_name) ) ENGINE=InnoDB COMMENT '考试计划表'; CREATE TABLE exam_course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plan_id BIGINT NOT NULL, course_name VARCHAR(50) NOT NULL, exam_type TINYINT DEFAULT 1 COMMENT '1笔试 2机考', total_minutes INT DEFAULT 120, UNIQUE KEY uk_course (plan_id, course_name) ) ENGINE=InnoDB COMMENT '考试课程表'; CREATE TABLE exam_student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id BIGINT NOT NULL, student_no VARCHAR(20) NOT NULL, student_name VARCHAR(30) NOT NULL, class_name VARCHAR(30) NOT NULL COMMENT '自然班', UNIQUE KEY uk_course_student (course_id, student_no) ) ENGINE=InnoDB COMMENT '考生名单表'; CREATE TABLE exam_room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(20) NOT NULL UNIQUE, building VARCHAR(30) NOT NULL, capacity INT NOT NULL DEFAULT 30, is_active TINYINT DEFAULT 1 ) ENGINE=InnoDB COMMENT '考场表'; CREATE TABLE exam_session ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plan_id BIGINT NOT NULL, course_id BIGINT NOT NULL, exam_date DATE NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, UNIQUE KEY uk_session (plan_id, exam_date, start_time, course_id) ) ENGINE=InnoDB COMMENT '考试场次表';

说明几个容易忽略的字段设计点。exam_student的联合唯一键(course_id, student_no)不是为了好看,是防止同一个学生被同一个课程重复录两次名单;很多真实事故都来自这份名单在导入时发生了两遍。exam_session的唯一键里放了plan_id + exam_date + start_time + course_id,它保证了同一课程在同一场次不会出现两个记录,也为后面的排考冲突检测提供了数据库层的兜底。exam_type字段标了笔试还是机考,因为二者在排考逻辑上不一样:机考通常跟着机房走,笔试才牵涉座位号。

2.3 第六张表:安排结果表与字段设计

排考结果单独建表,不要试图把考场号和座位号存进exam_student。原因是安排可能会被重新编排,单独的表可以保留历史记录,也方便回滚。安排结果表是整张表结构里最需要想清楚的一张。

CREATE TABLE exam_arrangement ( id BIGINT PRIMARY KEY AUTO_INCREMENT, session_id BIGINT NOT NULL, student_no VARCHAR(20) NOT NULL, student_name VARCHAR(30) NOT NULL, room_id BIGINT NOT NULL, seat_no INT NOT NULL, invigilator_id BIGINT, status TINYINT DEFAULT 0 COMMENT '0未发布 1已发布 2已调整', UNIQUE KEY uk_session_seat (session_id, room_id, seat_no), UNIQUE KEY uk_session_student (session_id, student_no) ) ENGINE=InnoDB COMMENT '考试安排结果表';

uk_session_seat约束的是同一个场次同一个教室同一个座位只能坐一个人;uk_session_student约束的是一个场次里一个学生只能有一条安排。这两条唯一键保证了即使业务代码写崩了,数据库也会拦住最严重的重复安排事故。invigilator_id直接挂在安排明细上,而不是单独建监考表,主要考虑是监考老师通常按教室指派,跟着明细走反而更好统计工作量。实际项目里如果老师要按场次轮换,可以再加一张exam_invigilator表,但最简版本挂在这里就够了。

前端页面的划分顺着这套表走就好:计划管理页负责exam_plan和exam_course,考生管理页负责exam_student导入,排考页先选场次再调exam_arrangement,成绩页可以复用exam_student加一个 score 字段,不必单独建成绩表。整个系统的边界就清楚了。

3. 排考引擎:场次安排、座位编排与监考分配的算法细节

3.1 先拆约束:排考不是排序,是带条件的填空

排考问题的本质是“把 N 个考生、M 个考场、K 个老师,填入 T 个时间段”,同时满足三类约束。硬约束是不能违反的:一个学生同一时间只能在一个教室;一个教室一个座位只坐一个人;一个老师同一时间不能监考两个教室;教室容量不能超员。软约束是尽量满足的:同一个自然班的学生尽量在同一个考场;监考老师的总监考次数尽量均衡;每个老师每天最多两场。

注意硬约束和软约束的实现方式完全不同。硬约束靠算法里的冲突检测和数据库唯一键双保险;软约束靠排序策略调节。很多考务系统翻车,就是把软约束当硬约束做,导致某一天排不出来,卡在死循环里。合理的做法是先保证硬约束通过,再用两轮分配的思路去逼近软约束。

3.2 排考主流程:先定场次,再排学生,最后分配监考

下面是我常用的一段排考核心逻辑,去掉业务无关的代码后,主流程可以精简成三步:遍历场次,把考生按班级分组,然后按“教室未满优先、班级集中优先”的原则落座。

// 伪代码:排考主流程,省略了事务包装和DTO public void arrangeSession(List<ExamStudent> students, List<ExamRoom> rooms) { // 按班级分组,保持同一自然班学生尽量同考场 Map<String, List<ExamStudent>> byClass = students.stream() .collect(Collectors.groupingBy(ExamStudent::getClassName)); List<ExamRoom> sortedRooms = rooms.stream() .filter(room -> room.getIsActive() == 1) .sorted(Comparator.comparingInt(ExamRoom::getCapacity).reversed()) .collect(Collectors.toList()); // roomUsedCapacity 记录每个教室当前已用座位数 Map<Long, Integer> roomUsed = new HashMap<>(); Map<Long, Integer> roomSeatCursor = new HashMap<>(); for (List<ExamStudent> classGroup : byClass.values()) { for (ExamStudent stu : classGroup) { boolean placed = false; for (ExamRoom room : sortedRooms) { int used = roomUsed.getOrDefault(room.getId(), 0); if (used < room.getCapacity()) { int seat = roomSeatCursor.getOrDefault(room.getId(), 1); // 落座并记录,seatNo 从1开始递增 saveArrangement(stu, room.getId(), seat); roomUsed.merge(room.getId(), 1, Integer::sum); roomSeatCursor.merge(room.getId(), 1, Integer::sum); placed = true; break; } } if (!placed) { throw new RuntimeException("考场容量不足,无法安排学生: " + stu.getStudentNo()); } } } }

三个参数在这里很关键。capacity按考试的课程类型从exam_room表读,机考课程的容量上限是机房机器数,笔试课程则按桌椅数减一预留机动座位。seatNo的设计我踩过坑:如果按学生遍历顺序直接 +1,会导致中间留位,所以单独维护一个roomSeatCursor游标。班级集中策略也有边界——如果某个班级人数超过最大教室容量,剩余的那几个人会散落到其他教室,这是可接受的,因为硬约束优先。真正需要重排的情况是“同一班级剩余人数超过剩余所有教室空位”,这时候只能扩场次,算法解决不了。

3.3 冲突检测的三个判定:考生冲突、教室冲突、教师冲突

算法写得再好,也拦不住多人协作时的手动调整——比如教务员手动把一个学生换到别的教室。所以冲突检测必须独立于排考流程存在,作为单独的校验接口每次保存前都跑一遍。三个判定条件按顺序排查,任何一个命中就拒绝保存。

第一,考生冲突。同一个exam_session里出现两个student_no相同但room_id或seat_no不同的记录。判定 SQL 用GROUP BY session_id, student_no HAVING COUNT(*) > 1即可,这条由uk_session_student唯一键兜底。第二,教室冲突。同一个session_id下同一个room_id出现两个相同seat_no。同样靠唯一键兜底。第三,教师冲突。这个最容易漏,因为invigilator_id在保存时可以规避同一场次重复,但两个不同场次时间重叠时老师可能被同时安排到两场。判断逻辑是:查同一老师所有安排场次,只要exam_date相同且start_time < 旧endTime AND end_time > 旧startTime,就说明时间重叠。

// 教师时间冲突校验,核心是区间重叠判定 public boolean hasTeacherConflict(Long teacherId, LocalDate date, LocalTime start, LocalTime end) { List<ExamArrangement> list = arrangementMapper .findByTeacherAndDate(teacherId, date); for (ExamArrangement existing : list) { LocalTime eStart = existing.getStartTime(); LocalTime eEnd = existing.getEndTime(); // 两个时间区间重叠判定 if (start.isBefore(eEnd) && end.isAfter(eStart)) { return true; } } return false; }

这段判定的核心是“左闭右开”的时间区间模型:start < eEnd && end > eStart,它把恰好首尾相接的两场考试视为不冲突,符合现实中连续监考两场的场景。这里必须用LocalTime,不能用字符串compareTo,因为"09:00"和"9:00"的字符串排序结果会出问题。我建议所有时间字段在后端实体里都用LocalDate/LocalTime,数据库字段用DATE/TIME,这两个类型配合 MyBatis-Plus 不需要额外转换器,比TIMESTAMP省很多事。

4. 用 Excel 批量导入考生名单:模板、POI 校验与编码三个坑

4.1 模板设计:四列数据是底线,还要一个隐藏规则

考生名单导入是每个教务员都用得上的功能,也是“看起来简单、做起来一地鸡毛”的典型模块。模板我基本固定为四列:学号、姓名、自然班、课程名称。为什么不要班级代码?因为导入者不一定记得住编码体系,反而容易填错。自然班直接用“计科2101”这种可读文本,后端按课程名称把学生归到对应的exam_course下。

模板里最重要的规则是:表头固定行、数据从第 2 行开始、空行跳过、学号列必须是文本格式。学号用文本格式是关键——很多 Excel 文件里 15 位以上学号会变成科学计数法,比如1.23457E+11,导入时读出来就是一串乱码。避免的办法是后端统一用DataFormatter取单元格的显示文本,而不是直接调getNumericCellValue()。另外模板第一行放一个示例行也可以,但导入逻辑必须把它过滤掉,我一般要求模板只放表头,不放示例数据,从源头杜绝脏数据。

4.2 POI 逐行读取与校验:每行错误都要能定位

导入解析我用 Apache POI 的WorkbookFactory读取,逐行校验,任何一行出错都不能静默跳过。下面这段代码是读取和校验的骨架,重点在错误信息能精确到行号和学号。

// 考生名单Excel导入,逐行校验并入库 public ImportResult importStudents(MultipartFile file, Long courseId) { ImportResult result = new ImportResult(); Set<String> seen = new HashSet<>(); // 内存去重 List<ExamStudent> list = new ArrayList<>(); try (Workbook wb = WorkbookFactory.create(file.getInputStream())) { Sheet sheet = wb.getSheetAt(0); DataFormatter fmt = new DataFormatter(); for (int i = 1; i <= sheet.getLastRowNum(); i++) { Row row = sheet.getRow(i); if (row == null || isBlankRow(row)) continue; // 跳过空行 String studentNo = fmt.formatCellValue(row.getCell(0)).trim(); String name = fmt.formatCellValue(row.getCell(1)).trim(); String className = fmt.formatCellValue(row.getCell(2)).trim(); if (studentNo.isEmpty()) { result.addError(i, "学号为空"); continue; } if (!studentNo.matches("\\d{6,20}")) { result.addError(i, "学号格式异常: " + studentNo); continue; } if (!seen.add(studentNo)) { result.addError(i, "学号重复: " + studentNo); continue; } list.add(new ExamStudent(courseId, studentNo, name, className)); } } catch (Exception e) { result.setFatal("文件解析失败: " + e.getMessage()); return result; } if (result.hasError()) { return result; // 有一行错误就整体拒绝入库 } // 批量插入,返回成功条数 examStudentMapper.batchInsert(list); result.setSuccessCount(list.size()); return result; }

这里的参数值得说明。seen是内存去重集合,只解决当前文件内的重复;库里还有uk_course_student唯一键拦着跨文件重复,双保险。isBlankRow判断某一行的五列都为空时返回 true,防止导入者拖拽填充造成大量尾行空数据。matches("\\d{6,20}")是学号格式的简洁校验——如果学校学号含字母,改成[A-Za-z0-9]{6,20}即可。还有一个参数容易被忽视:POI 读取 .xlsx 时是按行号遍历的,sheet.getLastRowNum()得到的是最后一个有内容的行索引,不是数据条数,所以循环里一定要配合isBlankRow跳过,否则中间某个空行会导致后续数据错位。

4.3 大数据量与编码兜底:文件超过一万行怎么办

考生名单经常是全校性的,一个文件几千行很正常。POI 的WorkbookFactory.create对 .xlsx 是内存解析,超过 5 万行就会明显卡顿,超过 10 万行有 OOM 风险。应对方案分两层:第一层是前端限制文件大小,超过 10MB 直接提示用户拆分导入;第二层是后端改用流式解析,XSSFWorkbook换成SAXParser走XSSFSheetXMLHandler,但代价是代码复杂度翻倍。我的建议是不要一上来就做流式,先把单次导入 5000 行以内跑通,确认业务没问题再加方案。

编码坑是另一个高频翻车点。有些模板是从教务系统导出的 CSV 改后缀变成 .xlsx 的,这种情况用 POI 打开会直接报“Invalid header signature”。此外 .xlsx 本质是 ZIP,如果在导入时前端把文件格式传错,后端要根据文件头判断而不是看扩展名。判断方法很简单:读文件前四个字节,50 4B 03 04是正常的 ZIP 文件头,D0 CF 11 E0是老版 .xls 的文件头。还有一类坑是 CSV 文件自带 BOM 头,第一列学号会被读成\uFEFF2021001,所以读到的第一列数据一定要先去掉\uFEFF前缀。这三个问题我在项目里全都踩过,现在统一用WorkbookFactory加DataFormatter加trim()组合拳解决。

5. 考务系统上线前的常见问题与避坑记录:5 个必须检查的场景

5.1 同一学生同一时间出现在两个教室:数据库唯一键救了场

现象: 排考完成后,导出考生名单,发现有个学生既在 A 考场又被安排到 B 考场。系统页面没报错,数据却已经落库了。

原因: 排考接口没有在事务内做考生维度冲突检测。两个并发请求同时进来,第一个请求还没提交,第二个请求查不到已有安排,于是重复插入。算法层没拦住,数据库层如果也没有唯一键,数据就脏了。

解决: 在exam_arrangement表加uk_session_student唯一索引,这是最底层的兜底。代码层面也要在保存前查一次selectCount(sessionId, studentNo),不是指望唯一键报错,而是为了给用户一个明确提示“该学生已安排过”。记住:唯一键是最后一道保险,不是唯一的手段。

5.2 Excel 导入中文乱码:不是 POI 的锅,是模板来源问题

现象: 用 POI 读取教务系统导出的报名表,中文字段全部变成乱码,但学号数字正常。

原因: 文件本身是 GBK 编码的 CSV,被导入者改后缀伪装成 .xlsx。POI 按 ZIP 格式解析必然失败;即使偶尔解析成功,也是因为某些文本编辑器把它当成了别的格式,读出来的字节流按 UTF-8 解码自然乱码。

解决: 后端不信任扩展名,读文件头判断真实类型。如果是 CSV,用InputStreamReader指定GBK解码;如果是真正的 .xlsx,POI 正常解析。这个用户看不到,但要不要做直接决定教务员使用体验。

5.3 监考工作量两极分化:先选老师再定教室,顺序反了必出问题

现象: 系统跑完排考,张三监考 8 场,李四只监考 1 场。教务员手动调整到崩溃。

原因: 监考分配是按教室顺序遍历的,第一个教室先到先得地把老师列表按固定顺序取走,越靠前的老师越容易被复用。没有全局考虑“谁监考的次数最少”。

解决: 分配监考时不再按教室循环,而是按场次循环,每个场次开始时把该场所有监考老师按“已监考次数”排序,取最少的几位。这个逻辑加在排考主流程里,跟在学生落座之后。每次排完一批次,立刻更新老师的已监考次数计数。

5.4 跨天时间比较出错:用字符串比时间,月底月初直接炸

现象: 考试安排在 3 月 31 日和 4 月 1 日两天,排考结果中 4 月 1 日的场次被校验逻辑误判成与 3 月 31 日冲突。

原因: 校验时间重叠时用了"2025-03-31 09:00".compareTo("2025-04-01 08:00")这类字符串比较。字符串比较按字典序对字符逐位比较,月和日位数固定时碰巧正确,但一旦日期格式换成2025-3-31这种不带前导零的格式,结果就错了。

解决: 所有日期字段用LocalDate,时间用LocalTime,比较用isBefore/isAfter,禁止字符串参与时间逻辑。我在代码里加了静态扫描规则,谁再写String.compareTo比较日期就报错,省得后人踩同一个坑。

5.5 跨浏览器支持:日期选择器在老内核上白屏

现象: 同一套前端,Chrome 正常,某个部门的老电脑用旧版 Edge 或低版本浏览器打开考试场次管理页,日期控件不显示,页面白屏。

原因: Element Plus 的日期选择器依赖现代浏览器 API,老内核不支持Intl.DateTimeFormat或某些 ES6 特性。项目只在一台机器上测过 Chrome,就以为全环境可用。

解决: 如果必须覆盖老浏览器,日期选择器不要用组件的默认实现,改用原生<input type="date">,它依赖的是浏览器内核基础能力,兼容性最好。前端构建时用 Babel 配置browserslist把兼容目标拉低。期末导出 Excel 文件名时,Content-Disposition里的中文也要用URLEncoder.encode转为 UTF-8 后再拼到 header 里,否则部分浏览器下载文件名会变成乱码。

6. 用模拟数据验收考务系统:构造场景、跑通排考、量化打分

排考模块写完了,怎么证明它能用?我习惯的做法是写一个模拟数据生成器,构造 8 个考场、20 门课程、200 名考生的数据,然后跑排考,最后用三条 SQL 做验收。这里有一套可以直接用的生成思路:考生学号从 20210001 递增,自然班按每 25 人一个班随机打散,考试场次分布在两个周末四天里,每天三场。模拟数据的价值在于把边界条件人为制造出来——某一天肯定会出现“考生总数大于当天考场总容量”的场景,排考算法必须给出教室不够的明确报错,而不是静默塞人。

验收分三层。第一层用 SQL 检查硬冲突是否存在:

-- 检查同一场次同一学生是否被重复安排 SELECT session_id, student_no, COUNT(*) FROM exam_arrangement GROUP BY session_id, student_no HAVING COUNT(*) > 1; -- 检查教室超员 SELECT a.session_id, a.room_id, COUNT(*) AS cnt, r.capacity FROM exam_arrangement a JOIN exam_room r ON a.room_id = r.id GROUP BY a.session_id, a.room_id, r.capacity HAVING COUNT(*) > r.capacity;

第二层用教师工作量统计:按invigilator_id分组统计监考场次,观察最多次数与最少次数的差值,控制在 2 场以内算合格。第三层做动态巡检:模拟“手动把一个考生从 A 教室移到 B 教室”的场景,看保存接口能不能正确拒绝座位冲突。这三层跑完,我心里基本有底。

收尾的进阶技巧:监考均衡不是平均主义,要给每个老师设置连续监考上限和单日上限两个参数。比如单日最多 2 场、连续监考最多 2 场。这两个参数在排考引擎里要作为软约束动态调整,而不是排完再人工改。实现方式是在教师分配轮询时,每次取该场次可用老师里“已监考场次最少且单日场次未满”的那位。这套逻辑我最初没做,上线后教务员手动调监考的时间比排考本身还长,后来补上才真正解放人力。做考务系统最容易低估的不是排考算法,而是“人工调整怎么办”——所以从设计第一天,就要给安排结果留出调整入口和校验接口。希望我的这些取舍,能让你在拿到类似代码包和需求的时候,少走几段绕路。

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

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

AUTOSAR网络管理启动时间优化:从NM状态机到BSW调度的实战指南

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

作者头像 李华
网站建设 2026/10/26 1:02:01

ESP32-S3 Dev Module Arduino开发实战:环境搭建、外设驱动与避坑指南

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

作者头像 李华
网站建设 2026/10/26 1:01:58

HTML/CSS/JS 示例源码:DOM、布局与响应式实战

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

作者头像 李华
网站建设 2026/10/26 1:01:21

微信小程序签到源码拆解:易打卡工程全链路实现与避坑指南

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

作者头像 李华
网站建设 2026/10/26 0:10:14

DeepSeek Harness:面向生产的Agent工程化操作系统

1. 项目概述&#xff1a;这不是又一个“玩具级”Agent框架&#xff0c;而是一套面向生产环境的工程化操作系统你有没有遇到过这样的场景&#xff1a;花三天时间搭好一个LangChain流程&#xff0c;跑通demo后信心满满地准备上线&#xff0c;结果第一周就发现日志里全是“LLM调用…

作者头像 李华
网站建设 2026/10/26 0:04:52

One-API多模型接口统一实践:从协议转换到智能调度

1. 为什么“多模型应用”刚起步就卡在了接口上&#xff1f;我去年下半年开始做一款面向中小企业的AI工作流工具&#xff0c;核心目标很朴素&#xff1a;让用户不用改一行代码&#xff0c;就能在同一个界面里自由切换调用 Qwen、GLM、Claude、DeepSeek、甚至本地部署的 Llama3。…

作者头像 李华