【项目总结】基于SSM+SpringBoot+Redis的个人博客系统项目总结
文章目录
- 项目介绍(开发背景)
- 数据库设计
- 主要使用到的技术点
- 前端
- 后端
- 自定义统一返回对象
- 自定义拦截器
- 加盐加密操作
- 分页功能
- session持久化
- 自定义头像的存储和获取
- 项目编写过程中遇到的困难点
- 困难点一(小)
- 困难点二(小)
- 困难点三(大)
- 上传部署
- 总结
项目介绍(开发背景)
对于一个程序员来说,定期整理总结并写博客是不可或缺的步骤,不管是对近期新掌握的技术或者是遇到bug解决bug过程的记录等,都是非常有必要的。
目前的博客网站有很多,比如CSDN、掘金、博客园等。本人在整个学习的阶段也都会经常在上面发布文章和见解等,最近学了一些开发所需要的主流框架,因此以做项目代学,做出了一个简易版的个人博客系统。
在这个博客系统中不需要担心安全问题(因为用户密码采用了加盐加密处理,破解成本高,且在一些私人界面加入了拦截器等)、短时间内不需要重复登录(因为在redis中存储了用户的session信息并进行了持久化处理)、可以随时随地地更换自己喜欢的头像(因为上传的图片发送到服务器并使用Nginx存储起来,稍等一段时间后台刷新后即可看到换的头像)。
数据库设计
由于这是一个简易版的博客系统,同时也是初代版本,因此在数据库的设计中还是比较简单的,只有两张表:用户表和博客表。后面做下一个版本的时候可以考虑加入文章下评论的表。
用户表主要存的字段是用户id、用户名、密码、头像、创建时间,其中主键是用户id且是自增的,编写出下面sql语句:
create table userinfo(id int primary key auto_increment,username varchar(100) not null,password varchar(100) not null,photo varchar(500) default '',createtime timestamp not null default current_timestamp,`state` int default 1
) default charset 'utf8mb4';
博客表主要存的字段是博客id、标题、内容、发布日期、发布用户、浏览量,其中主键是博客id且是自增的,编写出下面sql语句:
create table articleinfo(id int primary key auto_increment,title varchar(100) not null,content text not null,createtime timestamp not null default current_timestamp,uid int not null,rcount int not null default 1,`state` int default 1
)default charset 'utf8mb4';
主要使用到的技术点
前端
首先简单说一下本项目中前端代码的基本实现,由于是前后端分离的项目,所以前端需要发送请求给后端,后接收来自后端的响应,这里前端部分我基本使用的都是jQuery来实现的,而且也基本都是使用POST发送请求(相对来说会比较安全),但是也不是说POST就一定好,只是可能会使用的比较多,具体还是需要看实际项目的开发需求来进行制定。这里额外提供出一篇文章:GET和POST的区别。
后端
自定义统一返回对象
在主流的开发中基本上都会制定一套统一的返回数据形式,因为这样既可以方便后端程序员在返回数据的时候不需要考虑前端是如何接收的,又可以方便前端程序员在获取数据的时候不需要再去看后端返回的都有什么。不管传输的数据是何种形式,都可以一一种统一的格式被获取到,大大提高了开发的效率。
在返回对象之前需要先对数据进行一下封装,这一步称为“统一数据返回封装”,对于已经是封装好的对象直接返回即可;对于返回的类型是字符串类型的,则需要额外进行一步操作;对于未封装过的对象,直接对其进行封装后即可返回。而返回这个对象的这一步操作就称为“统一返回对象”。
ResponseAdvice类统一数据返回封装:
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@SneakyThrows@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {//本身已经是封装好的对象if(body instanceof HashMap){return body;}//返回的类型是字符创类型(String)if(body instanceof String){ObjectMapper objectMapper = new ObjectMapper();return objectMapper.writeValueAsString(AjaxResult.success(body));}return AjaxResult.success(body);}
}
AjaxResult类自定义统一返回对象:
public class AjaxResult {//业务执行成功时返回的方法public static HashMap<String, Object> success(Object data){HashMap<String, Object> res = new HashMap<>();res.put("code", 200);res.put("msg", "");res.put("data", data);return res;}public static HashMap<String, Object> success(String msg, Object data){HashMap<String, Object> res = new HashMap<>();res.put("code", 200);res.put("msg", msg);res.put("data", data);return res;}//业务执行失败时返回的方法public static HashMap<String, Object> fail(int code, String msg){HashMap<String, Object> res = new HashMap<>();res.put("code", code);res.put("msg", msg);res.put("data", "");return res;}public static HashMap<String, Object> fail(int code, String msg, Object data){HashMap<String, Object> res = new HashMap<>();res.put("code", code);res.put("msg", msg);res.put("data", data);return res;}
}
自定义拦截器
有一些页面是需要用户登录后才能打开的,如果未登录的用户强制打开就会跳转到登录页面,很多网站其实基本上也都是这样操作的,设置一个拦截器。
以下是设置自定义拦截器的一个模板:
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//session中得到用户信息(如果在session中得到userinfo对象,说明用户已经登录,否则用户未登录)HttpSession session = request.getSession(false);if(session != null && session.getAttribute(Constant.SESSION_USERINFO_KEY) != null){//当前用户已经登录return true;}response.setStatus(401);return false;}
}
@Configuration
public class AppConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns(excludes);}
}
addPathPatterns("/**")
是拦截项目中全部的url,而如果有一部分不需要拦截的话,只需要在excludePathPatterns(excludes)
中的excludes中添加不拦截url的集合即可。
为什么不直接设置拦截的url,而是先全部拦截后再开放一个集合呢?原因是在大部分常见的项目中,拦截的是要比不拦截的多很多,所以先拦截全部后再开放一部分显然是比较合理的。
对于本项目目前来说,设置不拦截的url有以下这些:
注意:前端代码一定是不能拦截的,不然页面会刷新不出来!
加盐加密操作
对于加密操作,可能会有很多人会想到MD5加密(因为学校教的就是MD5加密),MD5加密确实是不可逆的,但是它会存在一个问题,每次加密的结果都是一样的,这样的话黑客是可以通过一张“彩虹表”来暴力枚举破解密码,风险还是比较大的。
而加盐加密则不会,我们这里加的盐其实是随机生成的盐值,以下面这段代码为例:我们制定出一个64位的密码,其中前32位规定为是盐值的存储,后32位规定为是加盐加密后的密码,注册的时候将盐值和加盐加密后的密文拼接在一起存储在数据库中,而登录的时候则是取出密文中前32位的那个盐值和用户登录时输入的那个密码进行同规则的加密后,如果生成的密文与后32位的密文是相等的,那么说明登录成功,否则失败。
public class SecurityUtil {//加密操作public static String encrypt(String password){//每次生成内容不同但长度固定为32位的盐值String salt = UUID.randomUUID().toString().replace("-", "");String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());return salt + finalPassword;}//验证操作public static boolean decrypt(String password, String finalPassword){if(!StringUtils.hasLength(password) || !StringUtils.hasLength(finalPassword)){return false;}if(finalPassword.length() != 64){return false;}String salt = finalPassword.substring(0, 32);String securityPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());return (salt + securityPassword).equals(finalPassword);}
}
分页功能
分页功能实现的前提是需要先知道每一页需要显示多少条数据,也就是说,需要先计算出一个偏移量,以便于在MySQL查询中准确地进行获取数据并显示到页面。
@RequestMapping("list")public List<ArticleInfo> getList(Integer pindex, Integer psize){if(pindex == null || psize == null){return null;}int offset = (pindex - 1) * psize; //分页公式计算偏移量return articleService.getList(psize, offset);}
对应的MyBatis语句:
<select id="getList" resultType="com.example.demo.model.ArticleInfo">select * from articleinfo limit #{psize} offset #{offset}</select>
除此之外,还需要计算总页数,注意这是需要页数进行向上取整操作(数据数不满一页也需要计算成一页):
@RequestMapping("/totalpage")public Integer getTotalCount(Integer psize){if(psize != null){int totalCount = articleService.getTotalCount();return (totalCount + psize) / psize;}return null;}
session持久化
session持久化的目的是不需要让用户退出后短时间登录不再需要输入密码,因为这样会使用户体验更加好。
对于session的持久化其实不管是mysql还是redis都是可以实现的,但是目前主流的是存储到redis中,所以在此之前需要先提前安装并连接好redis(会在下篇文章中出教程)。
其中,存的话直接在用户登录的时候就可以将用户的session存起来,之后设置一个超时的时间(这部分是在配置文件中完成),由于不管是哪一个页面,都是需要对用户信息进行验证的,所以可以直接在验证的时候在redis中看看是否有存在符合的session即可。
session在redis中是这样存储的(如果过期则会在redis中清除):
自定义头像的存储和获取
在这一步中我想了很多办法,但是最终都是无法正确的实现头像的更换,最后是通过Nginx来搭建一个图床用来存储图片,之后这个图片就存在于公网中,也就可以很容易地被获取到了。
用户上传过来的图片命名格式是用户本地的设置名字,直接将这个文件名存起来的话,显然是不合适的,原因是如果有多个用户上传过来的图片内容不同但是文件名相同,这不就冲突了吗,所以,我们在获取到图片之后还需要就图片进行重命名。
对图片进行重命名有两种操作方式,一种是每次都随机生成一个UUID来代表文件名,这样就不会出现重复的现象,但是由于我们在后台就设置了不能注册相同的用户名,所以,我们也可以使用用户名来用作图片的命名,并且将这个名字+图片格式的后缀加到数据库中,其实这样还有一个好处,就是同一个用户对头像进行更换会对就图片进行覆盖,随机的UUID则不会,减少空间的占用(用户量大的时候)。
之后就是安装Nginx,并设置文件存储的路径。
@RequestMapping("/upload")public Object upload(HttpServletRequest request, MultipartFile file){HttpSession session = request.getSession( false);UserInfo userInfo = null;if(session != null && session.getAttribute(Constant.SESSION_USERINFO_KEY) != null) {userInfo = (UserInfo) session.getAttribute(Constant.SESSION_USERINFO_KEY);}if(file.isEmpty()){return AjaxResult.fail(-1, "请选择图片");}String originalFileName = file.getOriginalFilename(); //原来图片名String[] tmp = originalFileName.split("\\.");String ext = "." + tmp[tmp.length - 1]; //取到后缀String fileName = userInfo.getUsername() + ext;//上传图片String pre = "/www/wwwroot/43.139.71.60/blog_user_img/";String path = pre + fileName;try {//存到外部文件夹中file.transferTo(new File(path));userService.updatePhoto(fileName, userInfo.getId());return AjaxResult.success("更换成功!", 1);} catch (IOException e) {e.printStackTrace();}return AjaxResult.fail(-1, "更换失败");}
项目编写过程中遇到的困难点
因为这是我真正意义上做的第一个项目,所以在这个过程中遇到的困难点还是比较多的。
困难点一(小)
由于我一直以来都是以写后端代码为主,所以对于前端页面的制作相对来说会比较困难,很多时候会出现一些未知的错误,调式起来也会不太适应(通常都是会先用Postman先测试后端代码后再看前端如何修改等),但是后面写多了,其实也是有重复性的,慢慢地就适应了。
困难点二(小)
安装redis和远程连接redis客户端需要小心一点。
由于这个项目是临时想到要添加一个session的持久化的,使用也是临时在linux上安装redis的,过程中出现了大意,在开放了6379端口之后没有对redis设置密码,导致服务器被黑客入侵,后通过重装服务器解决。
教训:端口不要随便开放,特别是尽量不要全开,对于一些容易被黑客入侵的端口最好设置一个密码更好一点。
困难点三(大)
用户头像更换这个问题也是困扰比较久的,好在后来通过同学的指点和交流顺利解决了。
第一阶段我的思路是:因为用户那边上传过来的文件是会默认存放到target文件夹下的嘛,所以我使用了几个getParentFile()
方法以及查找的方法来将图片移动到static文件夹下的img文件夹里面,因为这样前端在获取图片的时候就可以直接获取到。当然,这个思路在本地运行是完全没有问题的,但是部署在linux上之后就会出现一个问题了,因为项目部署是需要打包的,而打的jar包在运行的时候执行getParentFile()
方法是会直接跳到整个SpringBoot项目外部的,图片存放位置就会出现错误,后来又查阅了很多相关的文章依旧没有解决这个问题……
第二阶段我的思路是:能不能在项目外找一个文件夹来存储这些图片呢?说干就干,在后端存储图片的时候直接就指定存储的路径,在项目启动之后,这次确实可以将图片成功存储进来,但是无论如何前端一直报错说获取不到这个图片,即使我在img标签下的src属性中使用的是绝对路径,也是不能获取到图片,第二种思路最后也是以失败告终……
第三阶段我的思路是:突然有一次我就想到了,不是还有图床这种东西吗,我能不能直接自己搭建一个图床呢?但是后来看到需要“氪金”的时候,作为“白嫖”的我也就放弃了……
第四阶段我的思路是:正当我想要通过“氪金”来解决这个问题的时候,有个同学(大佬)跟我说了可以使用Nginx来搭建自己的图床,而且还不需要“氪金”的时候,更换头像的难题也就迎刃而解了~
上传部署
当项目在本地能够顺利跑过之后,接下来就是部署到服务器上了。
在部署项目之前需要提前准备好的东西有:jdk1.8、redis、mysql及其库和表都需要建好。这里给出linux安装redis和mysql的教程(CentOS 7 通过 yum 安装 MariaDB、安装redis、记录远程客户端连接不上redis的解决等过程)。
完成上面的准备操作之后,就是部署项目了:将打好的jar包拉到服务器上,输入命令:java -jar [jar包名]
就可以打开SpringBoot项目;当然,这样是不能让项目持久运行的,只能当成一个提前的测试运行,持久运行需要输入命令:nohup java -jar [jar包名] &
。
总结
这是我真正意义上的第一个项目,有很多功能还没有实现,后面版本会持续进行更新……
简单总结一下这个博客系统项目:
优点:使用了主流的SSM+SpringBoot框架进行开发、使用Redis存储用户的session已达到持久化、对密码进行加盐加密处理、合理设置拦截器等。
缺点:页面制作的不够美观、有部分代码写的还是太过于冗余、实现的功能不太够等。
关于本项目的全部代码我都放在了我的个人Gitee账户下,有需要的可以点击查看:个人博客系统项目存放代码。(注:特别需要看其中的配置文件application.yml
以及所需要导入的依赖包pom.xml
,这决定了一个项目是否能够正常启动并运行!)
相关文章:

【项目总结】基于SSM+SpringBoot+Redis的个人博客系统项目总结
文章目录项目介绍(开发背景)数据库设计主要使用到的技术点前端后端自定义统一返回对象自定义拦截器加盐加密操作分页功能session持久化自定义头像的存储和获取项目编写过程中遇到的困难点困难点一(小)困难点二(小&…...
从入门到精通MongoDB数据库系列之一:MongoDB简介
从入门到精通MongoDB数据库系列之一:MongoDB简介 一、易于使用二、易于扩展三、功能丰富四、性能卓越五、设计理念MongoDB是功能强大、灵活且易于扩展的通用型数据库。融合了二级索引、范围查询、排序、聚合以及地理空间索引等诸多特性。 一、易于使用 MongoDB是面向文档的数…...

大数据系列——什么是hdfs?hdfs用来干什么的?
一、什么是HDFSHDFS全称是Hadoop Distributed File System是一种分布式文件系统(HDFS使用多台计算机存储文件,对外提供统一操作文件的接口)Hodoop使用HDFS(Hadoop Distributed File System)作为存储系统。二、hdfs用来干什么的用于大规模数据的分布式读写࿰…...

云端地球2月更新了这些功能,你都用过了吗?
时光飞逝、转眼已到2023年的第三个月,武汉的天气也逐渐转好,温度步步高升。云端地球产研团队的脚步也越走越快,虽然春节仿佛还是昨天的事,但云端地球已经完成了四次迭代,为广大建模爱好者带来了更多实用功能࿰…...

基于gin-vue-admin[gin+gorm]手动实现crud(全)
使用Gin-Vue- Admin框架手动实现crud 在gva框架下自己手动实现一个CRUD的操作,该操作将会结合gen进行探讨学习,具体实现可以看下面代码的实现,项目目录层级分为api层,service层,model层,common层ÿ…...

彻底关闭Windows10更新!!
以下四个步骤都需要执行。 一、禁用Windows Update服务 1、同时按下键盘 Win R,然后输入 services.msc ,点击确定。 2、找到 Windows Update 这一项,并双击打开。 3、双击打开它,点击 停止,把启动类型选为 禁用&…...

跨时钟域CDC
https://www.cnblogs.com/icparadigm/p/12794483.html https://www.cnblogs.com/icparadigm/p/12794422.html 亚稳态 是什么 时序逻辑在跳变时,由于异步信号、跨时钟域等原因,不满足setup或hold条件,输出在0和1之间产生振荡。 原因 D触发…...

JavaEE简单示例——Spring的控制反转
简单介绍: 在之前的入门程序中,我们简单的介绍了关于Spring框架中的控制反转的概念,这次我们就来详细的介绍和体验一下Spring中的控制反转的理论和实操。 使用方法: 控制反转(IoC)是面向对象编程中的一个…...

DBT 收购 Transform,指标平台已成现代数据栈关键拼图
今年 2 月初,现代数据技术栈独角兽 DBT 宣布完成对 Transform 的并购。在现代数据栈的体系中,DBT 和 Transform 都扮演着重要角色,DBT 侧重于整个分析链路上的数据转换处理,而 Transform 则聚焦在以指标为中心搭建业务分析应用。 …...
@Value注解取不到值的几种错误
在程序中使用了yml文件,然后把有些参数写在里面作为全局变量,在定时器里面使用,但是后来发现取不到: @Value("${spring.datasource.druid.master.url}") private String url; @Value("${spring.datasource.druid.master.driver-class-name}") private …...

听客户说|东台农商银行:建立健全数据安全管理制度的探索与实践
夯实银行数据安全,需“规划先行、谋定后动”,首要工作是确立管理工作的行动纲要,并据此建立制度保障体系以贯彻纲要,而后才是具体的行动措施和日常检查、监测。从银行数据安全建设实践路径来说,我认为可以用“盘现状、…...
Benchbot环境安装记录
https://github.com/qcr/benchbot 第一次安装这种复合型的环境,包括了各种CUDA/NVIDA驱动、Docker环境、python环境等等。因此,遇到了一大堆的问题,在此记录一下亲测有效的博客: https://zhuanlan.zhihu.com/p/378894743 https:/…...
Barrett模乘与蒙哥马利模乘算法
一、背景 公钥密码学(Public-Key Cryptography, PKC)由Diffie与Hellman于1970年代提出,在现代信息社会中得到了广泛应用。此后基于各种数学困难问题,越来越多的公钥密码算法被设计出来,比如RSA、ElGamal、椭圆曲线ECC算法等。在RSA算法中,模幂(modular exponentiation)…...
slice方法
slice方法与splice方法相比slice方法不会修改原数组一、语法以及描述说明:通过start 和 end对原数组进行浅拷贝(提取 start 至 end 索引的数组元素)语法:Array.slice([start[, end])参数:start : 开始索引end : 结束索…...

DevOps工具集合
简介 DevOps(Development和Operations的组合词)是一组过程、方法与系统的统称,用于促进开发(应用程序/软件工程)、技术运营和质量保障(QA)部门之间的沟通、协作与整合。 它是一种重视“软件开…...

手把手教你安装Linux!!!
文章目录Linux简述它们的区别安装CentOS①下载CentOS②安装Linux有两种方式③下载模拟软件④安装vmware⑤创建虚拟机⑥安装操作系统Linux简述 在国内比较流行的两款Linux发行版本CentOS和ubuntu 它们的区别 ubuntu:页面更加的华丽比较漂亮,它对计算机…...

图像分割(Unet算法学习笔记)
知识提要 数据集使用VOC2012 CNN 卷积神经网络Convolutional Neural Network GPU图像处理单元Graphic Processing Unit)图形处理器 convolution 卷积 ReLU全名Rectified Linear Unit,意思是修正线性单元 bn全称Batch Normalization批标准化 FC全连接神经网络是一种…...

Fortinet 发布《2022下半年度全球威胁态势研究报告》,七大发现值得关注
全球网络与安全融合领域领导者Fortinet(NASDAQ:FTNT),近日发布《2022 下半年度全球威胁态势研究报告》。报告指出,相对于组织攻击面的不断扩大以及全球威胁态势的持续演进,网络犯罪分子设计、优化技术与战术…...
ThinkPHP 6.1 模板篇之循环和选择标签
本文主要介绍在视图模板中,如何使用循环和选择标签去渲染变量及常用循环和选择标签。 目录 循环标签 foreach 标签 for 标签 volist 标签 选择标签 switch 标签 if 标签 范围标签 原生标签 总结 循环标签 foreach 标签 将查找到的数组或数据集ÿ…...

Jetpack太香了,让开发效率提升了不少
作者:Jingle_zhang 第三方App使用Jetpack等开源框架非常流行,在Gradle文件简单指定即可。然而ROM内置的系统App在源码环境下进行开发,与第三方App脱节严重,采用开源框架的情况并不常见。但如果系统App也集成了Jetpack或第三方框架…...

关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案
问题描述:iview使用table 中type: "index",分页之后 ,索引还是从1开始,试过绑定后台返回数据的id, 这种方法可行,就是后台返回数据的每个页面id都不完全是按照从1开始的升序,因此百度了下,找到了…...

[10-3]软件I2C读写MPU6050 江协科技学习笔记(16个知识点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16...
解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错
出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上,所以报错,到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本,cu、torch、cp 的版本一定要对…...
Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!
一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...

tree 树组件大数据卡顿问题优化
问题背景 项目中有用到树组件用来做文件目录,但是由于这个树组件的节点越来越多,导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多,导致的浏览器卡顿,这里很明显就需要用到虚拟列表的技术&…...

听写流程自动化实践,轻量级教育辅助
随着智能教育工具的发展,越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式,也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建,…...
LangChain知识库管理后端接口:数据库操作详解—— 构建本地知识库系统的基础《二》
这段 Python 代码是一个完整的 知识库数据库操作模块,用于对本地知识库系统中的知识库进行增删改查(CRUD)操作。它基于 SQLAlchemy ORM 框架 和一个自定义的装饰器 with_session 实现数据库会话管理。 📘 一、整体功能概述 该模块…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...

在Mathematica中实现Newton-Raphson迭代的收敛时间算法(一般三次多项式)
考察一般的三次多项式,以r为参数: p[z_, r_] : z^3 (r - 1) z - r; roots[r_] : z /. Solve[p[z, r] 0, z]; 此多项式的根为: 尽管看起来这个多项式是特殊的,其实一般的三次多项式都是可以通过线性变换化为这个形式…...

Chrome 浏览器前端与客户端双向通信实战
Chrome 前端(即页面 JS / Web UI)与客户端(C 后端)的交互机制,是 Chromium 架构中非常核心的一环。下面我将按常见场景,从通道、流程、技术栈几个角度做一套完整的分析,特别适合你这种在分析和改…...