news 2026/9/28 15:14:34

基于SpringBoot+Vue的前后端分离教育管理平台开发实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于SpringBoot+Vue的前后端分离教育管理平台开发实践

1. 项目背景与技术选型思考

去年接了一个面向基层党组织的教育学习管理平台开发任务,需求听起来并不复杂:管理员维护课程、学员在线学习、记录学时、在线考试、统计积分。但实际做下来,这绝不是一个简单的CRUD项目,涉及多角色权限、学习进度追踪、考试答题、数据统计,还要考虑后续可能接入移动端,所以我在技术选型上直接锁定了这套前后端分离方案:SpringBoot + Vue + MyBatis + MySQL。

先说说选型的逻辑。做后台管理系统,套装方案不少,JSP时代那种单体应用就不提了。前后端分离的核心价值在于:后端只提供RESTful接口,前端用Vue这类SPA框架承担页面渲染和交互,两端可以并行开发,互不阻塞。这个项目里,后端同学可以专注处理学习记录、考试成绩、权限控制这些逻辑,前端同学专心做页面,联调时只要按约定好的接口格式对接就行。团队协作效率高只是其一,更关键的是以后如果要接小程序或APP,后端接口可以直接复用,不用重新开发一套。

SpringBoot作为后端框架就不用多说了,最直观的好处是省掉了大量XML配置,内嵌Tomcat让本地开发和线上部署都简单很多,打包成一个jar直接运行。MyBatis在这个场景下比MyBatis-Plus和JPA更可控,因为教育管理业务里有大量动态查询报表,比如按时间段统计学习时长、按组织维度汇总考试通过率,这类SQL变化很多,MyBatis的XML动态SQL可以精确控制每一条查询语句,调优起来也方便。MySQL的选取很直接,项目数据量没有到需要上分库分表的程度,MySQL稳定且运维成本低,配合Navicat做日常管理足够。

技术栈里还有一个容易被忽略的关键点——这套组合的学习资料很多,遇到问题能搜到大量解决方案。实际开发过程中,JWT认证、Vue路由守卫、MyBatis分页这些通用问题前人已经踩平了路,技术债务基本可控。最终定下来的整体架构是:前端Vue2 + Element UI构建管理后台SPA,后端SpringBoot 2.7提供接口,MyBatis负责数据库访问,MySQL 8存储数据,认证走JWT,部署用Nginx托管前端静态资源并反向代理后端接口。

2. 数据库设计:从用户表如何长出几十张关联表

2.1 我的建表思路

这个系统的业务对象其实很集中:人(用户)、学习内容(课程)、行为(学习记录/考试)、管理(组织/菜单/权限/公告),但关系比较复杂。一上来我就确定了几条原则:

第一,权限相关表单独成组,与业务表完全分离。系统至少有两类角色:组织管理员和普通学员,管理员还能分不同层级的管理单位,所以RBAC权限模型是必需的,用户表、角色表、菜单表、用户角色关联表单独设计,互不交叉。

第二,学习业务要体现"过程"。仅仅记录"学过/没学"是不够的,组织方希望看到学习进度、学习时长、考试得分这些细节,所以学习记录表要同时记录多个维度的字段。

第三,所有表统一用自增主键、逻辑外键关联,保持建表习惯一致。时间字段一律用datetime,金额、进度、学时这种小数用decimal,避免浮点数误差。

第四,字符集统一utf8mb4。这一点吃过亏,早期用utf8,遇到课程标题里的生僻字或者特殊符号直接存入失败,换成utf8mb4之后再没出现过类似问题。

按照这个思路,核心表主要分为四大块:系统管理(sys_user、sys_role、sys_menu、sys_user_role)、组织架构(sys_dept)、学习资源(edu_category、edu_course、edu_video)、学习行为(edu_study_record、edu_exam_paper、edu_exam_record、edu_notice、edu_study_score)。

2.2 核心表的字段设计与取舍

用户表是最基础的,字段上除了常规账号密码,我特意加了dept_id关联组织表,方便按组织维度做数据隔离。密码存的是BCrypt加密后的串,绝对不能明文,后端登录时用BCryptPasswordEncoder比对原始密码和加密串。

CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(100) NOT NULL COMMENT 'BCrypt加密密码', `real_name` varchar(50) DEFAULT NULL COMMENT '姓名', `portrait` varchar(255) DEFAULT NULL COMMENT '头像URL', `dept_id` bigint DEFAULT NULL COMMENT '所属组织ID', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1启用 0禁用', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';

课程表是学习内容的载体。这里的学时段sort字段很有心机——管理员录入课程时可以设置课程学习必须达到的时长(比如45分钟),学员看完就等于完成学时,这个字段直接参与了后面的学分计算。视频地址我存的是相对路径,完整访问地址由后端拼接返回,这样将来迁移服务器或者换视频域名时不用改数据库。

CREATE TABLE `edu_course` ( `id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL COMMENT '课程标题', `cover_url` varchar(255) DEFAULT NULL COMMENT '封面图路径', `video_url` varchar(255) DEFAULT NULL COMMENT '视频路径', `type_id` bigint DEFAULT NULL COMMENT '课程分类ID', `required_hours` decimal(5,1) DEFAULT NULL COMMENT '规定学习时长(小时)', `content` text COMMENT '课程简介', `status` tinyint DEFAULT '1' COMMENT '状态:1上架 0下架', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习教育课程表';

学习记录表是整个系统的核心业务表,它记录每个学员和每门课的关系。我用唯一索引(user_id, course_id)保证同一用户同一门课只有一条记录,学习进度更新时直接UPDATE而不是INSERT新行,每次观看时长累加到duration字段。

CREATE TABLE `edu_study_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '用户ID', `course_id` bigint NOT NULL COMMENT '课程ID', `progress` int NOT NULL DEFAULT '0' COMMENT '学习进度百分比', `duration` int NOT NULL DEFAULT '0' COMMENT '累计学习时长(秒)', `exam_status` tinyint NOT NULL DEFAULT '0' COMMENT '考试状态:0未考 1已考', `exam_score` decimal(5,2) DEFAULT NULL COMMENT '考试成绩', `study_status` tinyint NOT NULL DEFAULT '0' COMMENT '学习状态:0学习中 1已完成', `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_course` (`user_id`, `course_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习记录表';

关于外键,我整个项目一张物理外键都没建,全部靠应用层保证数据一致性。原因有两个:一是物理外键在批量导入、初始化数据时容易制造麻烦,删改的顺序卡得很死;二是生产环境做分库分表时外键会成为障碍。这是业界常见做法,对多数互联网和后台项目都适用,只要开发时注意先删子表再删主表就行。

2.3 菜单与权限的数据库表达

菜单表用的是一棵父子树,通过parent_id自关联。字段包含路由路径、组件路径、图标、排序、是否显示。菜单数据初始化时我直接手动INSERT,不搞动态SQL生成,因为菜单是相对稳定的资源,管理员动态增删菜单的需求很小,避免过度设计。

CREATE TABLE `sys_menu` ( `id` bigint NOT NULL AUTO_INCREMENT, `parent_id` bigint DEFAULT '0' COMMENT '父菜单ID', `menu_name` varchar(50) NOT NULL COMMENT '菜单名称', `path` varchar(200) DEFAULT NULL COMMENT '前端路由地址', `component` varchar(200) DEFAULT NULL COMMENT '前端组件路径', `icon` varchar(100) DEFAULT NULL COMMENT '图标', `sort` int DEFAULT '0' COMMENT '显示排序', `visible` tinyint DEFAULT '1' COMMENT '是否显示', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜单权限表';

角色表和菜单表是多对多关系,通过sys_role_menu关联。用户从角色拿到菜单权限集合,前端根据这个集合动态生成左侧菜单,后端再在接口层用注解校验细粒度权限,两层配合下来权限才不会漏。

3. 后端开发的重头戏:SpringBoot与MyBatis的落地细节

3.1 项目分层与统一返回结构

后端我把包结构分成了controller、service、mapper、entity、dto、config、common、utils这几层。entity对应数据库表,dto接收前端传参,两者不混用。数据传输遵循这样一个约定:前端传参用dto,后端返回用vo,避免直接把数据库实体塞给前端,防止多查了password这类敏感字段。

统一返回结构是所有接口的基操。我写了一个Result类,包含code、message、data三个字段。成功时code固定为200,业务失败时code为500,未认证时code为401。前端axios拦截器拿到响应后统一判断code,这样每个接口函数不用重复写成功失败逻辑。

public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.code = 200; r.message = "success"; r.data = data; return r; } public static <T> Result<T> error(String message) { Result<T> r = new Result<>(); r.code = 500; r.message = message; return r; } }

全局异常处理也非常重要。不用try-catch把Controller包得全是异常,而是写一个@RestControllerAdvice,方法里抛出的所有异常统一到这里处理。业务异常我自定义了一个BusinessException,代码里发现参数不对等场景直接就throw,由全局处理拦截后转换成Result返回。这样Controller层代码非常干净。

3.2 JWT认证与权限控制的完整逻辑

登录认证走的是JWT方案。用户提交用户名密码后,后端用BCryptPasswordEncoder验证密码,成功后生成一个Token返回前端,Token里包含userId、username、角色标识这些关键信息,有效期设置为24小时。生成Token用的密钥放在application.yml配置里,不要写死在代码中,部署时用环境变量覆盖。

Token的拦截在SpringBoot拦截器里做。我写了一个JwtInterceptor,注册时指定拦截路径为/api/**,排除登录接口。拦截器做的事有三件:

  1. 从请求头Authorization里取Token,没有直接返回401。
  2. 解析Token,校验签名和有效期,失败返回401。
  3. 解析成功就把userId和角色信息放到ThreadLocal里,供后面的业务代码随时取用。

权限控制则在Controller方法上打注解,比如@RequirePermission("course:add"),通过Spring AOP拦截,查看当前用户角色拥有的菜单权限里是否包含这个权限标识。这套RBAC方案比Shiro轻量,又比纯手写宽松,足够支撑这种管理系统的权限需求。

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new BusinessException(401, "请先登录"); } Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); if (claims == null) { throw new BusinessException(401, "登录状态已过期,请重新登录"); } UserContext.set(claims.get("userId", Long.class), claims.get("role", String.class)); return true; } }

3.3 MyBatis动态SQL与分页插件的正确用法

MyBatis构建查询条件非常灵活。课程列表页往往要支持关键字搜索、分类筛选、状态筛选、时间排序,如果每个组合都写一个SQL方法显然不现实,动态SQL的where标签就能解决。

<select id="selectCoursePage" resultType="cn.demo.entity.EduCourse"> SELECT * FROM edu_course <where> <if test="keyword != null and keyword != ''"> AND title LIKE CONCAT('%', #{keyword}, '%') </if> <if test="typeId != null"> AND type_id = #{typeId} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY create_time DESC </select>

注意这里为什么用CONCAT拼接模糊查询而不是直接写'%${keyword}%',因为$传参会直接拼SQL字符串,存在注入风险,#是预编译占位符,安全。

分页直接用PageHelper这个分页插件。用法很简单,在mapper查询前一行调用PageHelper.startPage(pageNum, pageSize),后面紧跟的查询语句就会自动拼接LIMIT,同时返回一个Page对象可以取总数。

PageHelper.startPage(pageNum, pageSize); List<EduCourse> list = courseMapper.selectCoursePage(queryDto); PageInfo<EduCourse> pageInfo = new PageInfo<>(list);

实际使用中有个坑:PageHelper只对紧随startPage后的第一条查询语句生效,如果那后面不小心先执行了别的查询,分页就乱了。所以我会把PageHelper.startPage写在Service层,紧挨着真正要分页的mapper调用之前,中间不做任何其他数据库操作。

后端还有两个高频功能点是文件上传和定时统计。文件上传用MultipartFile接参,限制单个文件大小在Spring配置里设置。定时统计用Spring自带的@Scheduled注解,每天凌晨统计前一天的学员学时数据并更新汇总表,注意定时任务发方法要加@Async避免阻塞主线程,生产环境还要注意集群部署时多个节点同时执行定时任务会造成重复计算,这个项目因为单节点部署暂时没处理。

4. 前端Vue:从脚手架到接口联调的每一步

4.1 项目初始化和目录结构

前端用的Vue2生态,脚手架直接vue create admin-ui创建,UI组件库选的Element UI(当时Element Plus还不够成熟,Vue3的坑也还没填完)。项目结构如下:

  • api目录:按业务模块拆分的接口文件
  • views目录:页面组件
  • router目录:路由配置
  • store目录:Vuex状态管理
  • utils目录:axios封装和工具函数
  • components目录:公共业务组件

跑起来之前必须先确认Node环境和npm源。我用nvm管理Node版本,项目指定Node 16,npm源切到国内镜像,不然创建项目时下载依赖能等半小时。

4.2 axios封装与Token处理

axios封装是整个前端联调的基石。核心注意点有两个:请求拦截器统一附加Token,响应拦截器统一处理业务码和HTTP错误码。

import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 15000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { Message.error(res.message || '请求失败') if (res.code === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(new Error(res.message || 'Error')) } return res }, error => { if (error.response && error.response.status === 401) { Message.error('登录已过期,请重新登录') localStorage.removeItem('token') router.push('/login') } else { Message.error(error.message || '网络异常') } return Promise.reject(error) } ) export default service

baseURL用的是环境变量VUE_APP_BASE_API,开发环境指向http://localhost:8080/api,生产环境这个值会被nginx代理转发逻辑覆盖,不用改代码。

4.3 路由守卫与动态权限菜单

路由守卫控制未登录用户无法访问内部页面,在每次路由跳转前检查localStorage里有没有token,没有就弹回登录页。这是第一层前端防护,虽然真正的权限校验在后端,但前端不能直接让用户看到无权限的页面。

动态菜单的实现稍微复杂一点:用户登录成功后,后端根据角色返回该用户可见的菜单列表,前端把这棵菜单树存到Vuex里,在路由跳转后的回调里根据当前路径找到对应组件渲染。持有不同角色的用户,看到的侧边栏菜单天然不同。

这里我踩过一个坑:动态菜单如果用静态路由表一次性注册所有页面,用户即使没有权限访问,也能通过URL直接跳转到达页面组件。解决方式是路由表只保留登录页和404页和基本框架页,其余页面组件全部通过后端菜单列表动态注册,这样URL访问没有权限的路由时会落到404。

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path === '/login') { next() } else { if (!token) { next('/login') } else { if (!store.state.hasGotMenu) { store.dispatch('getMenuList').then(() => { next({ ...to, replace: true }) }) } else { next() } } } })

4.4 典型业务页面的实现要点

课程学习页是前端最有技术含量的部分。视频播放用的video.js插件,学习时长上报写了一个定时器,每10秒向后端上报一次当前观看进度。这个上报接口设计成幂等更新,后端拿到用户ID和课程ID直接UPDATE学习记录表。考试页面则是把题目从后端一次性取回(题量控制在50题以内),前端用radio组件渲染单选题,多选题用checkbox,答完提交时前端只负责把答案组装成JSON传给后端,由后端统一判分,前端不参与分数计算,防止有人篡改。

后台管理页面主要是表单和表格的组合。Elment UI的el-table+b分页组件配合后端分页接口,页面切换时重新请求数据,不是把全量数据拉到前端再前端分页,数据量大时前端分页会卡成幻灯片。查询条件做成表单提交,点击查询时重置pageNum为1再请求,这个细节忘了的话就会出现"在第5页按条件搜索搜不到结果"的问题。

联调阶段我踩得最狠的坑是跨域。开发环境下前端跑在8080端口,后端跑在8080,两边端口不一致,浏览器直接报跨域。我用了Vue CLI的devServer代理,把/api开头的请求转发到后端地址。生产环境则是由Nginx统一代理,彻底规避跨域。

5. 部署上线实录:前端dist与后端jar如何配合

5.1 前后端各自的打包方式

后端打包我用的Maven,执行mvn clean package -DskipTests,跳过测试直接产出jar包。打包前记得把application.yml里的profile切到生产配置,数据库连接信息改成服务器上的MySQL地址。

mvn clean package -DskipTests nohup java -jar edu-admin.jar --spring.profiles.active=prod > app.log 2>&1 &

前端打包就一条命令,npm run build,产出dist目录。这个目录就是整个前端站点的静态资源,把它上传到服务器指定目录,比如/home/www/edu/dist,后面交给Nginx解读。

build之前要关注一个文件:vue.config.js里的publicPath。如果部署在域名根路径,publicPath设成'/';如果部署在子路径,比如https://xxx.com/edu/,就要设成'/edu/',否则JS和CSS全部404,页面白屏。

5.2 Nginx配置与反向代理

服务器我用的CentOS 7 + Nginx,安装没什么说的,重点在配置。整个配置的核心思路:Nginx监听80端口,收到请求后如果是静态资源的路径就从dist目录取文件,如果是/api开头的路径就反向代理到后端jar端口。

server { listen 80; server_name example.com; root /home/www/edu/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

这里有个非常关键的配置:location /里的try_files $uri $uri/ /index.html。因为Vue是SPA单页应用,路由切到/course时实际上后端并没有这个物理路径,如果Nginx不去找index.html,就会直接404。加上这一句,所有不存在的路径都会回退到index.html,由Vue的路由接管渲染,这才算真正能跑起来。

反向代理方面,我把/api统一转发到后端的http://127.0.0.1:8080。后端Controller的路由设计就要配合这个约定,所有接口路径必须以/api开头。我遇到过有人后端接口直接写成/course/list,前端请求/api/course/list,后端怎么都接收不到,就是路径前缀对不上。

5.3 单机部署与后续延伸

这套配置跑通的是最基础的单机部署:一台服务器上同时部署Nginx、MySQL 8、Jar包。服务器配置建议至少2核4G,MySQL和Java进程都吃内存,1G内存会频繁触发交换分区导致接口响应变慢。

我实际部署时用的是云服务器加域名的方式,Docker方案也验证过,但考虑到维护简易度和团队熟悉程度,最终生产选择了直接跑Jar包。如果在Windows上本地演示,后端同样打成Jar包运行,前端dist目录扔到任意静态服务器即可,浏览器访问静态服务器地址,接口请求走Nginx代理或者前端直接配后端地址加允许跨域都行。

如果团队熟悉容器化,下一步可以把这套环境迁移成docker-compose编排,MySQL、后端、Nginx各一个容器,配置会清爽很多,但这是项目稳定运行之后的事了。

6. 复盘与踩坑:那些让项目差点延期的问题

6.1 MyBatis字段映射与时间字段的坑

第一个坑是MyBatis的驼峰映射问题。数据库字段用的是下划线命名,比如create_time,Java实体类用的是驼峰命名createTime,如果MyBatis全局配置没开启驼峰映射,查出来的create_time字段永远为null。解决方案是在application.yml里加一行:

mybatis: configuration: map-underscore-to-camel-case: true

加了这行后MyBatis自动将下划线字段名转换为驼峰实体属性名,常见问题但新人容易忽略。另一个相关的坑是数据库表字段评论和代码实体类字段必须严格对应,不然写动态SQL时稍微错一个字母,运行期才报错。

第二个坑是MySQL时区问题。部署后前端页面显示时间和本地时间差了8个小时,排查了一圈发现是JDBC连接串没指定时区。MySQL 8默认时区是UTC,本地开发环境没注意,上了服务器时间就对不上。修复方式是连接参数加serverTimezone=Asia/Shanghai,同时数据库连接池配置里统一时区设置。

jdbc:mysql://127.0.0.1:3306/edu_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

6.2 前端Long类型精度丢失

这个坑是在联调阶段突然被发现的。后端用户表主键是bigint自增,但到了第19位的时候前端JavaScript的Number类型精度不够,ID末尾几位变成了0。比如数据库里存的ID是1534891200303108096,前端拿到后却显示成1534891200303108000。这导致涉及用户详情、课程详情的操作全部错乱。

理解原因后一下就明白了:Java的Long是64位整数,JS的Number是双精度浮点,能精确表示的最大整数是2^53-1,超过这个值就会丢精度。解决方案是我在后端统一把Long类型字段序列化时转成字符串,让前端接收到的是字符串而不是数字。

@JsonSerialize(using = ToStringSerializer.class) private Long id;

或者在Jackson全局配置中把Long转String序列化。这个坑在有大量自增主键的后端项目中很典型,一定要注意。

6.3 文件上传大小限制与Nginx的配置联动

课程封面、附件这些上传文件一开始明明没超过限制,但还是报413错误。排查后发现有两层限制:SpringBoot默认限制单文件1MB,我把配置改成了20MB;但Nginx那边也有一个client_max_body_size参数,默认是1m,如果不改,请求到达SpringBoot之前就被Nginx拦下来了。配置加在nginx.conf的http块或server块里。

client_max_body_size 50m;

这个经历提醒我:多一层代理就多一层限制,排查问题不能只看应用本身,要顺着请求链路一个一个环节查。

6.4 Nginx代理后获取用户真实IP的坑

日志记录里发现所有用户的请求IP都显示成127.0.0.1,这是因为Nginx反代之后后端拿到的是代理服务器的地址。解决方式是在Nginx配置中添加X-Real-IP和X-Forwarded-For请求头,后端用HttpServletRequest.getHeader("X-Real-IP")获取真实IP。如果不用这些头,后面做登录日志、操作审计时IP数据就没法看了。

这个项目从前端脚手架搭建到最终部署上线,前后大概用了三周,其中第一版上线后修复各种小问题用了一周。说实话,这套系统并没有用到特别高深的技术,但每个环节都踩了一遍很经典但也很有代表性的坑。总结下来,前后端分离项目的核心不只是把页面和接口分开,而是要在设计阶段就想清楚权限怎么控制、跨域怎么处理、部署怎么配合,这几件事想清楚了,后面写代码都很快。如果你正在做类似的管理系统,这几个关键点提前想明白,是可以少走不少弯路的。

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

Redis设置密码全攻略:配置文件、Docker容器、命令行三场景

Redis 设置密码&#xff08;配置文件、docker容器、命令行3种场景&#xff09;半夜两点被告警叫醒&#xff0c;Redis 实例 CPU 打满&#xff0c;登录服务器一看&#xff0c;几千个 key 被清空&#xff0c;还多了几个奇怪的 cron 任务。再一查&#xff0c;redis-cli -h 公网IP连…

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

旧显卡拆解:碳族元素在电路板中的隐藏角色

前阵子清理工作室&#xff0c;从抽屉底翻出一块退役多年的旧显卡。散热器一拆&#xff0c;露出来一整片墨绿色的电路板&#xff0c;上面密密麻麻趴着电容、电感、芯片、晶振&#xff0c;还有一些叫不出名字的黑色小方块。说实在的&#xff0c;这块板子已经没什么实际用途了&…

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

GitHub热榜拆解:AI Agent记忆层开源项目实战

GitHub热榜从2026-09-22那天开始&#xff0c;连续挂着一批和“智能体记忆层”相关的仓库。热搜词里一水儿的GitHub字眼&#xff0c;但真正值得琢磨的不是榜单本身&#xff0c;而是这批项目背后集中爆发的一个信号&#xff1a;AI Agent正在从“每次对话都失忆”往“带着长期记忆…

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

STM32F4+INMP441数字麦克风I2S音频采集与双缓冲DMA实现

做音频采集这个方向&#xff0c;我把市面上常见的模拟麦克风方案都试了个遍&#xff0c;电路噪声、运放增益、偏置电阻&#xff0c;每一步都在跟模拟电路搏斗。后来换成INMP441这颗I2S数字MEMS麦克风&#xff0c;一下就清爽了——音频数据直接以数字信号从I2S接口送进STM32F4&a…

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

C++过滤器模式实战:从if-else地狱到可扩展过滤链

1. 过滤器模式&#xff1a;从堆if-else到可扩展的处理链1.1 过滤器模式到底解决了什么问题C里的过滤器模式&#xff0c;说直白点就是把一条处理逻辑拆成一串可以单独替换的关卡&#xff0c;让数据依次经过每一道关卡做筛选和加工。它不是什么高深的设计模式&#xff0c;但在日志…

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

Java IO流核心原理与实战:字节流、字符流、编码与缓冲机制详解

做Java开发这些年&#xff0c;IO流是绕不开的一座山。你刚接触时觉得它抽象&#xff0c;学了一段时间又觉得它琐碎&#xff0c;什么字节流、字符流、InputStream、Reader&#xff0c;名目繁多。但你真正吃透这套体系之后会发现&#xff0c;它不过就那几根柱子&#xff1a;数据从…

作者头像 李华