尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
基于Java的学生选课管理系统:从技术选型到部署避坑的完整指南
简介这份资源是面向高校计算机专业学生与Java Web初学者的一套学生选课管理系统完整项目资料围绕教学管理场景解决课程发布、选课退课、成绩录入与权限控制等实际业务问题适合作为课程设计、毕业设计或Java Web入门练手参考。压缩包共5个文件包含2个zip源码与数据库脚本、2个mp4部署演示视频以及1个sql文件整体约208.74MB源码、建库脚本与部署录屏相互配套便于对照理解项目从搭建到运行的完整链路。目前已有152人学习下载。读者可从中获取基于Spring Boot与MySQL的三层架构实现思路涵盖登录注册、课程管理、并发选课冲突处理、退课与成绩管理等模块并借助部署视频完成JDK、Tomcat与数据库环境配置快速跑通系统、排查常见问题是理解Java Web实际项目开发流程的实用素材。1. 学生选课管理系统为什么“能跑通”和“能交付”是两回事每年毕业季计算机专业的学生都会扎堆做同一类题目——学生选课管理系统。标题里写着“基于 Java”“源代码”“数据库”“部署视频”看起来像是一个打包好的课程设计资源。但真正做过的人都知道这类系统最难的从来不是写代码而是让它在别人的机器上也能跑起来。你本地用 IDEA 跑得好好的换一台电脑数据库连不上、JDK 版本不对、Tomcat 端口被占随便一个环节都能卡住半天。这篇文章面向两类人一是正在做课程设计、需要一套能讲清楚、能演示、能答辩的选课系统的学生二是刚入行、想通过一个完整项目理解 Java Web 开发全流程的初级开发。我会把“基于 Java 的学生选课管理系统”拆成可落地的技术方案从技术选型、数据库设计、核心业务实现到部署上线的完整路径再到那些只有踩过才知道的坑。源代码和数据库脚本是结果但更重要的是理解每一步为什么这么做。部署视频能帮你省时间但如果你不知道背后的配置逻辑换个环境照样翻车。2. 技术选型为什么这套组合拳最适合课程设计与企业入门2.1 后端为什么选 Spring Boot MyBatis 而不是纯 Servlet很多课程设计的默认方案是 JSP Servlet JDBC理由是“老师只教了这个”。但如果你想让项目在简历上拿得出手或者答辩时能多讲几句Spring Boot MyBatis 是更务实的选择。Spring Boot 的内嵌 Tomcat 让你不需要单独装服务器main方法直接启动MyBatis 把 SQL 从 Java 代码里抽出来改查询不用重新编译。具体依赖版本上JDK 用 8 或 11 都行Spring Boot 选 2.7.x 系列最稳MyBatis Starter 用 2.3.x。不要追最新版Spring Boot 3.x 要求 JDK 17很多学校机房环境跟不上。数据库驱动用mysql-connector-java8.0.x连接 MySQL 5.7 和 8.0 都兼容。!-- pom.xml 核心依赖 -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies !-- Web 层提供 REST 接口和内置 Tomcat -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 持久层MyBatis 与 Spring Boot 整合 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- 简化实体类编写 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这段配置里spring-boot-starter-parent统一管理了所有依赖的版本号你不需要在每个依赖里写version。mybatis-spring-boot-starter会自动扫描Mapper注解的接口省去手动配置SqlSessionFactory的麻烦。Lombok 用Data替代 getter/setter代码量能少三分之一。2.2 前端用 Thymeleaf 还是前后端分离课程设计场景下我建议用 Thymeleaf 做服务端渲染。原因很直接你不需要额外学 Vue 或 React不需要处理跨域不需要单独部署前端项目。一个 Spring Boot 的 jar 包丢过去就能跑部署视频里也不用讲 Nginx 配置。但如果你想让项目看起来更“现代”可以用 Vue Element UI 做前后端分离。代价是你要多写一套 axios 请求封装多配一个跨域过滤器部署时要么把前端打包进static目录要么单独起一个 Nginx。对于答辩只有十分钟的场景Thymeleaf 的性价比更高。# application.yml 关键配置 spring: datasource: url: jdbc:mysql://localhost:3306/course_selection?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false # 开发阶段关缓存改页面不用重启 prefix: classpath:/templates/ suffix: .html mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true # 数据库 snake_case 自动映射 Java camelCaseserverTimezoneAsia/Shanghai这个参数不加MySQL 8.0 会报时区错误。map-underscore-to-camel-case开启后数据库的student_id会自动映射到 Java 的studentId不用在 SQL 里写别名。thymeleaf.cachefalse在开发阶段必开否则你改了 HTML 刷新页面看不到变化会以为代码写错了。2.3 数据库选 MySQL 5.7 还是 8.0两者在选课系统这个量级下没有本质区别。MySQL 8.0 的窗口函数和 CTE 在这个场景用不上但 8.0 的默认字符集是utf8mb4支持 emoji5.7 默认是latin1建库时不指定会出乱码。如果你用 5.7建库语句必须写DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci。另一个坑是驱动类名。MySQL 5.7 用com.mysql.jdbc.Driver8.0 用com.mysql.cj.jdbc.Driver。用错了启动时报Loading class com.mysql.jdbc.Driver is deprecated虽然还能跑但日志里一堆警告。直接统一用com.mysql.cj.jdbc.Driver5.7 也兼容。3. 数据库设计五张表撑起选课核心业务3.1 表结构设计与字段类型选择选课系统的核心表就五张学生表、教师表、课程表、选课记录表、用户表。用户表存登录信息学生和教师表存业务信息通过user_id关联。选课记录表是核心中的核心它决定了“一个学生同一门课只能选一次”这个约束。-- 用户表所有角色的登录入口 CREATE TABLE sys_user ( user_id int NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT MD5加密后的密码, role varchar(20) NOT NULL COMMENT STUDENT/TEACHER/ADMIN, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 学生表关联用户表存学号、班级等 CREATE TABLE student ( student_id int NOT NULL AUTO_INCREMENT, user_id int NOT NULL, student_no varchar(20) NOT NULL COMMENT 学号, name varchar(50) NOT NULL, class_name varchar(50) DEFAULT NULL, major varchar(50) DEFAULT NULL, PRIMARY KEY (student_id), UNIQUE KEY uk_student_no (student_no), KEY fk_student_user (user_id), CONSTRAINT fk_student_user FOREIGN KEY (user_id) REFERENCES sys_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表存课程名、学分、容量、授课教师 CREATE TABLE course ( course_id int NOT NULL AUTO_INCREMENT, course_name varchar(100) NOT NULL, credit decimal(3,1) NOT NULL DEFAULT 0.0, capacity int NOT NULL DEFAULT 50 COMMENT 课容量, selected_count int NOT NULL DEFAULT 0 COMMENT 已选人数, teacher_id int DEFAULT NULL, semester varchar(20) DEFAULT NULL COMMENT 开课学期, PRIMARY KEY (course_id), KEY fk_course_teacher (teacher_id), CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher (teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课记录表学生与课程的多对多关系 CREATE TABLE course_selection ( id int NOT NULL AUTO_INCREMENT, student_id int NOT NULL, course_id int NOT NULL, select_time datetime DEFAULT CURRENT_TIMESTAMP, status tinyint DEFAULT 1 COMMENT 1-已选 0-已退, PRIMARY KEY (id), UNIQUE KEY uk_student_course (student_id,course_id), KEY fk_selection_course (course_id), CONSTRAINT fk_selection_student FOREIGN KEY (student_id) REFERENCES student (student_id), CONSTRAINT fk_selection_course FOREIGN KEY (course_id) REFERENCES course (course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_student_course这个唯一索引是关键。它从数据库层面保证了同一个学生不能对同一门课产生两条选课记录。没有这个约束你只能在 Java 代码里查一次再插入并发场景下两个请求同时查都说不存在然后都插入数据就脏了。selected_count字段是冗余设计用空间换时间选课时直接更新这个字段比每次COUNT(*)快得多。3.2 选课容量控制乐观锁还是悲观锁选课系统最典型的并发场景是一门课剩最后一个名额两个学生同时点选课。如果代码写成“先查容量再插入再更新计数”两个请求都查到容量没满都插入成功最后selected_count变成 51超卖了。常见做法有两种。悲观锁用SELECT ... FOR UPDATE锁住课程行事务提交前其他请求阻塞。优点是简单缺点是并发高时大量请求排队响应变慢。乐观锁用版本号或直接UPDATE course SET selected_count selected_count 1 WHERE course_id ? AND selected_count capacity根据影响行数判断是否成功。// 乐观锁方案选课核心逻辑 Transactional public String selectCourse(Integer studentId, Integer courseId) { // 先检查是否已经选过唯一索引兜底这里只是友好提示 int count selectionMapper.countByStudentAndCourse(studentId, courseId); if (count 0) { return 你已经选过这门课; } // 原子更新只有容量未满时才增加计数 int updated courseMapper.increaseSelectedCount(courseId); if (updated 0) { return 课程已满选课失败; } // 插入选课记录 selectionMapper.insert(studentId, courseId); return 选课成功; }对应的 MyBatis SQLupdate idincreaseSelectedCount UPDATE course SET selected_count selected_count 1 WHERE course_id #{courseId} AND selected_count lt; capacity /updateincreaseSelectedCount返回的是影响行数。如果返回 0说明selected_count capacity课程已满。这个判断和更新在一条 SQL 里完成是原子操作不存在并发间隙。Transactional保证选课记录插入失败时计数回滚。注意lt;是 XML 里小于号的转义写法直接写会解析报错。3.3 退课与重选的数据一致性退课不是DELETE而是把status改成 0同时selected_count减一。这样做的原因是保留选课历史方便统计和审计。重选时先查有没有status0的记录有就更新为 1没有才插入新记录。-- 退课软删除 计数减一 UPDATE course_selection SET status 0 WHERE student_id ? AND course_id ? AND status 1; UPDATE course SET selected_count selected_count - 1 WHERE course_id ? AND selected_count 0;selected_count 0这个条件防止计数变成负数。虽然正常流程不会出现但如果有管理员手动删了选课记录却没更新计数退课时就会触发。加上这个条件至少不会把数据搞得更乱。4. 核心功能实现从登录拦截到选课接口的完整链路4.1 登录与权限拦截Session 还是 JWT课程设计场景下Session 足够用而且实现简单。用户登录成功后把user对象放进HttpSession写一个HandlerInterceptor拦截所有/student/**、/teacher/**、/admin/**的请求检查 Session 里有没有用户信息没有就重定向到登录页。// 登录拦截器 public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object user session.getAttribute(loginUser); if (user null) { // 未登录重定向到登录页 response.sendRedirect(request.getContextPath() /login); return false; } return true; } }注册拦截器时要注意排除登录页、注册页和静态资源Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /doLogin, /register, /css/**, /js/**, /images/**); } }addPathPatterns(/**)拦截所有请求excludePathPatterns放行登录相关和静态资源。如果不放行/css/**登录页的样式加载不出来页面会变成纯文本。这个坑我见过不止一次排查半天以为是路径写错了其实是拦截器把 CSS 请求也拦了。4.2 选课接口的参数校验与异常处理选课接口接收courseId从 Session 里拿studentId。参数校验用RequestParam的requiredtrue保证courseId不为空业务校验在 Service 层做。Controller RequestMapping(/student) public class StudentController { Autowired private CourseService courseService; PostMapping(/selectCourse) ResponseBody public Result selectCourse(RequestParam(courseId) Integer courseId, HttpSession session) { User user (User) session.getAttribute(loginUser); if (user null) { return Result.error(请先登录); } try { String msg courseService.selectCourse(user.getUserId(), courseId); return 选课成功.equals(msg) ? Result.success(msg) : Result.error(msg); } catch (Exception e) { // 唯一索引冲突兜底 if (e.getMessage().contains(Duplicate entry)) { return Result.error(你已经选过这门课); } return Result.error(系统繁忙请稍后重试); } } }Result是一个统一返回类包含code、msg、data三个字段。前端根据code判断成功还是失败。Duplicate entry是 MySQL 唯一索引冲突的报错信息虽然前面已经查过一次但并发场景下两个请求同时通过检查第二个插入时会触发唯一索引这里捕获后返回友好提示避免 500 错误页。4.3 课程列表分页与模糊查询课程列表要支持按课程名模糊查、按教师名查、按学期查还要分页。用 PageHelper 插件最省事一行代码搞定分页。!-- pom.xml 添加 PageHelper -- dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependencypublic PageInfoCourseVO listCourses(String keyword, Integer pageNum, Integer pageSize) { PageHelper.startPage(pageNum, pageSize); ListCourseVO list courseMapper.selectByKeyword(keyword); return new PageInfo(list); }PageHelper.startPage必须紧跟在查询方法之前中间不能插入其他数据库操作否则分页会作用到错误的查询上。CourseVO是视图对象比实体类多一个teacherName字段通过LEFT JOIN teacher查出来。select idselectByKeyword resultTypecom.example.vo.CourseVO SELECT c.*, t.name AS teacherName FROM course c LEFT JOIN teacher t ON c.teacher_id t.teacher_id where if testkeyword ! null and keyword ! AND (c.course_name LIKE CONCAT(%, #{keyword}, %) OR t.name LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY c.course_id DESC /selectwhere标签会自动处理第一个AND不用写WHERE 11。CONCAT(%, #{keyword}, %)是 MySQL 的字符串拼接用#{}预编译防止 SQL 注入。如果用${}拼接输入 OR 11就能查出所有数据。5. 部署避坑从本地跑通到换机可用的五条血泪经验5.1 数据库连接失败时区、驱动、权限三连坑现象本地 IDEA 启动正常换一台电脑导入项目后启动报Communications link failure或Access denied for user。原因第一种是 MySQL 8.0 的时区问题serverTimezone没配或配错。第二种是驱动类名用了旧版com.mysql.jdbc.Driver。第三种是数据库用户没有远程访问权限只能localhost连接。解决连接 URL 统一写成jdbc:mysql://localhost:3306/course_selection?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse。驱动类名用com.mysql.cj.jdbc.Driver。如果换机后连不上先ping数据库 IP再用mysql -u root -p -h IP测试远程连接不通就检查user表的host字段是不是%。5.2 端口占用8080 被占用的快速排查现象启动时报Port 8080 was already in use。原因本机已经有一个 Tomcat 或 Spring Boot 应用在跑或者上次启动的进程没杀干净。解决Windows 下netstat -ano | findstr :8080找到 PIDtaskkill /PID xxx /F杀掉。Mac 或 Linux 用lsof -i:8080然后kill -9 PID。更省事的办法是在application.yml里改端口server.port: 8081但部署视频里要提醒观众改端口后访问地址也要跟着变。5.3 静态资源 404拦截器放行规则写漏了现象登录页能打开但 CSS 和 JS 全部 404页面样式全乱。原因拦截器addPathPatterns(/**)把静态资源请求也拦了而excludePathPatterns里没写/css/**、/js/**。解决在excludePathPatterns里补上静态资源路径。如果用了 Thymeleaf/templates/**下的 HTML 不需要放行因为它们是服务端渲染后返回的。但/static/**下的资源必须放行。Spring Boot 默认把classpath:/static/映射到根路径所以/css/style.css实际找的是static/css/style.css。5.4 中文乱码从数据库到页面的全链路排查现象课程名在数据库里显示正常页面上是问号或乱码。原因数据库字符集、连接 URL 字符集、页面编码三者不一致。常见的是数据库建库时用了latin1或者连接 URL 没写characterEncodingutf8。解决建库语句必须写DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci。连接 URL 加useUnicodetruecharacterEncodingutf8。Thymeleaf 页面meta charsetUTF-8。如果还乱码检查 MySQL 的character_set_server变量SHOW VARIABLES LIKE character%看是不是utf8mb4。5.5 选课计数不一致手动改数据后的修复脚本现象课程列表显示的已选人数和实际选课记录数对不上。原因有人直接改了course_selection表的数据但没有同步更新course.selected_count。或者退课时只改了status没减计数。解决写一个修复脚本以course_selection表为准重新计算。-- 修复 selected_count 与实际选课记录不一致 UPDATE course c SET c.selected_count ( SELECT COUNT(*) FROM course_selection cs WHERE cs.course_id c.course_id AND cs.status 1 );这条 SQL 在数据量不大时直接跑没问题。如果数据量大可以加WHERE c.selected_count ! (SELECT COUNT(*) ...)只更新不一致的行。跑之前先备份跑完用SELECT验证一遍。6. 从能跑到能讲答辩演示与二次开发的进阶技巧答辩演示最怕两件事一是演示到一半报错二是老师问“这个功能怎么实现的”答不上来。我的习惯是提前准备一个“演示专用”数据库里面预置好学生、教师、课程和选课记录演示时只走正常流程不现场造数据。选课演示要选一门容量充足的课不要选只剩最后一个名额的万一并发演示时翻车场面会很尴尬。二次开发最常见的需求是加“选课时间窗口”限制。实现方式是在course表加select_start和select_end两个datetime字段选课前判断当前时间是否在窗口内。// 选课时间窗口校验 public String selectCourse(Integer studentId, Integer courseId) { Course course courseMapper.selectById(courseId); LocalDateTime now LocalDateTime.now(); if (course.getSelectStart() ! null now.isBefore(course.getSelectStart())) { return 选课尚未开始; } if (course.getSelectEnd() ! null now.isAfter(course.getSelectEnd())) { return 选课已结束; } // ... 后续选课逻辑 }另一个常见需求是导出选课名单为 Excel。用 EasyExcel 最简单加一个依赖写一个ExcelProperty注解的 VO 类三行代码导出。// 导出选课名单 public void exportSelection(Integer courseId, HttpServletResponse response) throws IOException { ListSelectionExportVO list selectionMapper.selectExportByCourseId(courseId); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment;filenameselection.xlsx); EasyExcel.write(response.getOutputStream(), SelectionExportVO.class).sheet(选课名单).doWrite(list); }SelectionExportVO里用ExcelProperty(学号)、ExcelProperty(姓名)标注字段导出的表头就是中文。response.setContentType必须设对否则浏览器会当成 HTML 打开而不是下载。最后说一个我自己的教训不要等到答辩前一天才部署到演示电脑上。我见过太多人本地跑得好好的到了教室发现 JDK 版本不对、MySQL 没装、端口被占。提前一天在演示环境完整走一遍把数据库脚本、JDK、Maven 依赖都准备好比多写两个功能重要得多。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

通过 Rube MCP 自动化 ParseHub:parsehub-automation Skill 实战指南

通过 Rube MCP 自动化 ParseHub:parsehub-automation Skill 实战指南

AI 技能AI 插件人工智能工作流自动化 【免费下载链接】awesome-claude-skills A curated list of awesome Claude Skills, resources, and tools for customizing Claude AI workflows 项目地址: https://gitcode.com/GitHub_Trending/aw/awesome-claude-skills 点击…

📅 2026/10/4 10:53:00
动力总成悬置解耦计算:MATLAB与Adams双路线实战对比

动力总成悬置解耦计算:MATLAB与Adams双路线实战对比

做动力总成悬置设计的朋友,对解耦计算应该再熟悉不过了。这两年我经常收到类似的私信:“悬置解耦到底用MATLAB算还是Adams算?”“两款工具算出来的结果差别大不大?”“有没有现成的计算流程能直接抄?”今天就把我在实际…

📅 2026/10/4 10:53:00
Rust 的赋能哲学:从 TRPL 前言看内存安全、无畏并发与跨领域编程

Rust 的赋能哲学:从 TRPL 前言看内存安全、无畏并发与跨领域编程

教程文档 【免费下载链接】book The Rust Programming Language 项目地址: https://gitcode.com/gh_mirrors/bo/book 点击查看 免费下载 导读 本文以《The Rust Programming Language》(以下简称 TRPL,即本仓库 gh_mirrors/bo/book 所维护的…

📅 2026/10/4 10:48:00
MORE NEWS

更多资讯

📰

MRAM在工业控制器中的掉电安全存储方案:基于TM4C129的完整实现

做工业控制器的人,最怕的往往不是算法写不出来,而是好不容易跑起来的数据,断电或者写穿就没了。前两年我在一个产线数据记录仪项目里,用 TM4C129XKCZAD 搭配一颗 MR25H40CDF 做运行日志、标定参数和关键事件记录,这套组…

📰

Cursor 与 Stagewise 配合使用完全指南:Bridge 模式下的前端编码代理配置

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

📰

VSCode + ClaudeCode 配置冲突排查:settings.json 与 cc-switch 的 Base URL 改到 TaoToken

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

📰

MRAM工业数据存储实战:MR25H40CDF与PIC18LF4585 SPI驱动调试与避坑指南

MRAM 这东西,第一次接触的人多半会犯嘀咕:它到底算 RAM 还是 Flash?我在一个工业数据采集项目里用 MR25H40CDF 配 PIC18LF4585 做参数存储,前后折腾了将近三周,踩的坑比预想的多得多。这篇就把整个选型思路、SPI 时序调…

📰

LangChain4j Agentic Core Layer:构建生产级AI Agent系统

1. 项目概述:为什么一个库能“打全套”?你有没有遇到过这种场景:刚用 LangChain4j 写完一个带 Tool 的简单函数调用,第二天产品提需求——要支持多步骤决策、要接入企业知识库做 RAG、要并发处理 50 用户请求、还要把整个流程串进…

📰

OpenShell:为Windows 11开始菜单找回效率与自定义自由

一说Windows 11那套居中的开始菜单,不少人是又爱又恨。爱的是它确实比Windows 10时代更符合触屏逻辑,恨的是在鼠标和键盘操作下,它把效率踩在脚底下反复摩擦。打开全屏应用列表要滚好几屏,固定磁贴又没法塞下大量常用工具&#xf…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬