news 2026/9/29 2:02:48

Ubuntu服务器自建Git仓库实战:从SSH配置到Hook自动部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ubuntu服务器自建Git仓库实战:从SSH配置到Hook自动部署

1. 为什么自建Git仓库而不是直接用Gitee或GitHub

很多人听到"在服务器上搭建Git仓库"第一反应是:现在Gitee、GitHub这么方便,为什么还要自己折腾?这个问题我一开始也觉得犯不上,直到真正工作中遇到了几个场景。

第一个场景是内网环境。公司的研发网和公网隔离,代码出不去也进不来,但团队又想享受版本管理、分支合并这些Git带来的好处,这时候本地服务器就是唯一选择。第二个场景是私有仓库的代码量敏感。有些项目连私有托管平台都不想用,比如涉及客户数据的处理脚本,放哪里都觉得不踏实,自己服务器上划一块地方存,权限捏在自己手里。第三个场景是调试和自动化。自建仓库配合Hook可以做很多事情——push完代码自动部署到测试环境,自动打标签,触发构建,这在很多半自动化的团队里非常实用。

那Ubuntu是不是唯一选择?当然不是,CentOS、Debian都能干这活,但Ubuntu胜在两个点:一是用户基数大,遇到问题搜索解决方案几乎一查一个准;二是软件包版本比较新,Git官方维护的PPA也更新及时。我用的服务器是Ubuntu 22.04 LTS,下面所有操作都基于这个版本,如果你是20.04或者24.04,命令基本通用,个别差异我会在文中指出来。

还要说明一点:这篇文章不是让你在服务器上把GitLab全家桶装一遍。就我个人经验,80%的自建仓库场景根本用不到GitLab那么重的方案。一个裸仓库(bare repository)加SSH访问,就能解决绝大多数需求。装GitLab那个内存占用,我自己在2GB的小服务器上试过,跑起来卡得不行,后来老老实实换回裸仓库方案,省心太多。

所以这篇文章的路线是:服务器端怎么初始化环境、怎么建仓库、怎么配权限,然后是客户端怎么连接、日常提交的完整流程,最后是我实际使用中踩过的一些坑和几个让仓库更好用的进阶操作。无论你是个人开发者想把代码备份到自己的服务器,还是小团队内网协作,照着走一遍都能跑通。

2. 服务器端准备:从SSH到Git安装

2.1 第一步先搞定SSH连接

搭建Git仓库的前提是你能顺畅地操作这台服务器。我见过不少新手卡在这一步:系统装了,Git也装了,但本地连不上服务器,后面全白搭。

Ubuntu服务器装好之后,默认是开了SSH服务的。如果你装的是Desktop版本,那默认还真不一定装openssh-server,需要手动确认一下:

sudo apt update sudo apt install openssh-server -y sudo systemctl status ssh

看到active (running)就说明SSH服务在跑。然后在本机测试连接:

ssh 用户名@服务器IP

这里要提醒一个细节:root用户直接用密码登录在很多Ubuntu版本上默认是禁止的。我在配置服务器时通常会创建一个专门用户来管理Git仓库,而不是用root。原因后面讲权限的时候会细说。

2.2 用SSH密钥免密登录

搭建Git仓库之后,你会频繁地push/pull代码,如果每次连接都输密码,体验很差。更关键的是,用密钥认证比密码登录安全得多——私钥留本地,公钥放服务器,别人无法通过暴力破解密码闯进来。

生成本机的SSH密钥对:

ssh-keygen -t ed25519 -C "你的注释,比如邮箱或机器名"

现在新机器我基本用ed25519算法,比传统的RSA 2048短且安全。如果你的Git服务器比较老,或者客户端环境特殊,再用RSA:

ssh-keygen -t rsa -b 4096 -C "备注"

生成的公钥在~/.ssh/id_ed25519.pub,私钥在~/.ssh/id_ed25519。把公钥内容追加到服务器的授权列表:

ssh-copy-id 用户名@服务器IP

或者手动把公钥内容追加到服务器上~/.ssh/authorized_keys文件里。加完之后测试:

ssh 用户名@服务器IP

如果能直接登上去不用输密码,密钥认证就配好了。

2.3 安装Git并验证版本

接下来在服务器上装Git。Ubuntu的默认源里就有Git,直接:

sudo apt update sudo apt install git -y git --version

Ubuntu 22.04默认源的Git版本大概是2.34,日常用完全够了。如果你需要更新版本,可以用Git官方维护的PPA:

sudo add-apt-repository ppa:git-core/ppa sudo apt update sudo apt install git -y

装完后顺手配置一下服务器端Git的基本信息。这个配置其实主要用于git commit时记录作者信息,虽然服务器端很少直接提交代码,但建议还是配上:

sudo -u git git config --global user.name "git" sudo -u git git config --global user.email "git@服务器IP"

这里的git用户后面会详细介绍。

3. 创建Git仓库:选择目录结构与初始化方式

3.1 为什么推荐创建专门的git用户

在服务器上管理Git仓库,我强烈建议创建一个独立的系统用户,比如就叫git。这么做有几个好处:

  • 权限隔离。仓库文件归git用户所有,其他普通用户没有直接读写权限,只能通过Git协议访问。
  • 方便管理。所有仓库集中在/home/git或/srv/git下,一目了然。
  • 安全。即使某个Web应用被攻破,攻击者拿到的也不是root权限,无法直接篡改仓库。

创建用户:

sudo adduser --system --shell /usr/bin/git-shell --group git

这里用--shell /usr/bin/git-shell是一个安全增强。Git官方自带一个git-shell,它限制用户只能执行Git相关命令,不能登录服务器执行任意Shell命令。如果用户不小心公钥泄露,攻击者也没法通过这个账号拿到Shell。不过需要确认git-shell存在:

which git-shell

如果路径不对,可以手动指定一下,或者先不给--shell参数,创建成功后再改:

sudo usermod -s /usr/bin/git-shell git

3.2 初始化裸仓库

这是整个搭建过程最关键的一步。先解释一下什么叫裸仓库。普通的仓库有一个工作目录,你可以看到文件、修改文件;而裸仓库没有工作目录,它只保存Git的版本历史数据,相当于一个"纯服务器端"的仓库。我们push到服务器上的就应该是裸仓库,因为服务器不需要直接在这个目录里改文件。

创建裸仓库的命令:

sudo mkdir -p /srv/git sudo chown git:git /srv/git sudo -u git git init --bare /srv/git/awesome-project.git

注意仓库命名约定:通常以.git结尾,这样一看就知道是裸仓库。--bare参数就是创建裸仓库的意思。

创建完可以在本地验证一下:

ls /srv/git/awesome-project.git

你会看到HEAD、branches、config、objects、refs这些目录和文件。这就是一个最小的可用Git服务端。

3.3 分支与HEAD设置

裸仓库默认的分支通常叫master,现在Git社区普遍用main。我自己更喜欢把新仓库默认分支设为main,避免后面团队开发时产生分支命名混乱。设置方法:

sudo -u git git --git-dir=/srv/git/awesome-project.git symbolic-ref HEAD refs/heads/main

这行命令的作用是把裸仓库的HEAD指针指向main分支。如果设置了这一步,客户端第一次clone下来时默认分支就是main。

另外,如果你希望服务器端仓库允许接收任何分支的推送,保持默认就好。如果希望固定分支,可以配置config文件。不过对小型团队来说,保留默认行为更灵活。

3.4 仓库的目录规划建议

如果团队项目多,建议按一定的目录结构组织仓库。我的习惯是这样:

/srv/git/ ├── awesome-project.git ├── backend-service.git └── docs-site.git

每个项目一个裸仓库,互不干扰。配合后面讲的gitolite或gitea可以做到更细的权限控制,但对大多数人来说,裸仓库加SSH已经足够了。

4. 本地开发流程:Clone、Commit、Push与分支管理

4.1 从服务器克隆仓库

服务器端配好了,现在切到自己的开发机。

如果你是从零开始一个新项目,先在Gitee或者GitHub上建好项目也行,但我们要的是走自己的服务器,所以直接在本地初始化然后推送到远程。

本地初始化:

mkdir awesome-project cd awesome-project git init -b main echo "# awesome-project" > README.md git add README.md git commit -m "initial commit"

然后添加远程仓库地址:

git remote add origin git@服务器IP:/srv/git/awesome-project.git

这里的git@服务器IP表示以git用户的身份连接服务器,路径是/srv/git/awesome-project.git。第一次连接SSH可能会提示确认服务器指纹,输入yes即可。

推送:

git push -u origin main

-u参数设置上游分支,以后直接git push和git pull就可以,不用每次带远程名和分支名。

如果仓库里已经有代码了,可以用clone的方式:

git clone git@服务器IP:/srv/git/awesome-project.git

4.2 日常开发循环:add、commit、push

这是Git使用频率最高的三个命令。我见过很多初学者把顺序搞混,其实逻辑很简单:

  1. git status查看当前状态,看哪些文件改了。
  2. git add把改动加入暂存区。
  3. git commit把暂存区的内容提交到本地仓库。
  4. git push把本地提交推送到远程服务器。

实际命令:

git status git add index.html git commit -m "更新首页标题" git push

关于写commit message,我的建议是:不要用"update"、"fix"这种含混的标题,尽量写清楚这次改了什么、为什么要改。比如修复登录页在Safari下布局错乱的问题就比fix bug有价值得多。一个好的commit历史,可以在项目出问题时快速定位回滚点。

4.3 分支管理:从创建到合并

几乎每个团队都会有"主分支保持稳定,新功能在分支上开发"的需求。Git分支的开销非常小,创建合并都很方便。

创建并切换到新分支:

git checkout -b feature/login-page

等价于两条命令:

git branch feature/login-page git checkout feature/login-page

在新的分支上开发完成后,合并回主分支:

git checkout main git pull git merge feature/login-page git push

如果合并时有冲突,Git会在冲突文件里标记出<<<<<<<和>>>>>>>的区域,你需要手动解决这些冲突,再add、commit。这里有个技巧:合并前先git pull把自己本地仓库更新到最新,能减少很多冲突。

4.4 拉取远程更新:pull和fetch的区别

git pull和git fetch经常有人搞混。简单说:

  • git fetch只把远程的更新下载到本地,但不合并到你当前的分支。适合先看看别人改了什么再决定怎么处理。
  • git pull等于git fetch加git merge,直接拉取并合并到当前分支。日常开发中用pull更省事。

不过git pull在某些场景下会产生意外的合并提交。如果团队协作频繁,我建议用git pull --rebase,它会把你的本地提交"重放"到远程分支的最新提交之上,历史更线性,看起来也更清爽。

5. 多用户协同时的权限配置与常见报错

5.1 多用户共用git账号的权限方案

前面创建的git用户可以当作统一的Git访问入口。多个人要使用同一台服务器时,最简单的方式是把每个人的SSH公钥都添加到git用户的authorized_keys文件里。

具体做法是收集每个开发者的id_ed25519.pub公钥内容,然后追加到服务器上/home/git/.ssh/authorized_keys:

sudo -u git mkdir -p /home/git/.ssh sudo -u git touch /home/git/.ssh/authorized_keys # 编辑文件,把每个开发者的公钥逐行加入 sudo -u git vim /home/git/.ssh/authorized_keys

这样所有开发者的push都会以git用户身份操作。优点是配置简单,缺点是无法区分是谁提交的。但这个局限可以通过约束commit信息来解决,也可以在服务端配gitolite做更细粒度的权限管理。

如果你需要仓库级别的权限控制,比如A只能访问项目A,B只能访问项目B,那gitolite是轻量级的好选择。它基于git用户再包一层权限控制逻辑,用配置文件管理用户与仓库的访问关系。比GitLab轻太多,适合小团队。不过配置有学习成本,这篇文章先不多展开,基础的裸仓库方案已经能解决大部分问题。

5.2 推送失败:Permission denied (publickey)

这是新手最常见的问题。执行git push时提示:

Permission denied (publickey). fatal: Could not read from remote repository

排查步骤:

  1. 先确认本机的公钥有没有加到服务器的authorized_keys里:ssh -T git@服务器IP。如果显示Welcome to Git或Hi说明认证通过。
  2. 确认本机当前使用的SSH密钥是不是你添加的那把。有时候本机有多个密钥,而SSH默认用的不是你加的那把。可以在~/.ssh/config里指定:
Host mygit-server HostName 服务器IP User git IdentityFile ~/.ssh/id_ed25519
  1. 确认服务器上authorized_keys文件的权限。这个文件的属主必须是你连接的用户,且权限不能太宽松:
sudo chown git:git /home/git/.ssh/authorized_keys sudo chmod 600 /home/git/.ssh/authorized_keys sudo chmod 700 /home/git/.ssh

5.3 推送到非裸仓库时报错

如果你初始化仓库时没用--bare,而是用普通的git init /srv/git/awesome-project,推代码时大概率会遇到这个错误:

remote: error: refusing to update checked out branch

原因是服务器端仓库存在一个工作目录,Git默认拒绝在工作目录被检出的分支上接收推送,怕把你服务器上正在使用的文件搞乱。

解决办法有两种:

  • 推荐做法:重新用裸仓库。把普通仓库复制成裸仓库:
git clone --bare /srv/git/awesome-project /srv/git/awesome-project.git
  • 或者,如果你确实需要服务器端能checkout一份最新代码过去(比如用于部署),那可以把仓库设置成允许接收推送并自动更新工作目录,这个就是后面要讲的Hook自动部署。

5.4 文件权限导致的推送问题

有时候push能成功,但服务器上查看仓库文件时发现权限不对,其他用户无法读取。这通常是因为创建仓库时用的是root用户,导致仓库目录属主不是git用户。

解决办法:

sudo chown -R git:git /srv/git

这一步很容易被忽略,但影响了多人协作时能不能正常读写。

6. 进阶玩法:利用Hook实现自动部署

6.1 什么是Git Hook

Git Hook是Git在特定事件发生时自动执行的脚本。对于自建服务器来说,最常用的是post-receive——当服务器接收到一次push完成之后执行。这个Hook可以在push完成后自动把代码同步到指定的Web目录,实现"push即部署"。

这个功能解决的实际问题是:团队里经常有人push完代码后忘了去服务器上手动拉取更新,导致线上代码和仓库不一致。配置了自动部署之后,代码一到服务器,Web服务立刻就用上新版本了。

6.2 创建post-receive Hook实现自动部署

假设你的Web站点目录是/var/www/awesome-project,目标效果是:有人推送代码到仓库后,服务器自动把最新代码同步到这个目录。

第一次需要先准备部署目录:

sudo mkdir -p /var/www/awesome-project sudo chown -R www-data:www-data /var/www/awesome-project

然后在裸仓库的hooks目录里创建post-receive文件:

sudo -u git vim /srv/git/awesome-project.git/hooks/post-receive

写入以下内容:

#!/bin/bash TARGET="/var/www/awesome-project" GIT_WORK_TREE="$TARGET" git checkout -f

保存后赋予执行权限:

sudo chmod +x /srv/git/awesome-project.git/hooks/post-receive sudo chown git:git /srv/git/awesome-project.git/hooks/post-receive

原理是:把仓库的工作目录临时指定为/var/www/awesome-project,然后强制checkout最新代码。这个方案只适合静态站点或者纯前端项目。如果是Node、Python这种需要构建或重启服务的项目,还需要在Hook里加上重启服务、执行构建脚本的命令,逻辑会更复杂。

我实际项目中用的Hook比这个复杂一些,会先判断推到的是不是main分支,只有主干更新才触发部署:

#!/bin/bash TARGET="/var/www/awesome-project" while read oldrev newrev ref do if [ "$ref" = "refs/heads/main" ]; then echo "检测到main分支推送,开始部署..." GIT_WORK_TREE="$TARGET" git checkout -f # 在这里可以加上构建命令,例如: # cd $TARGET && npm install && npm run build sudo systemctl reload nginx else echo "非main分支推送,跳过自动部署" fi done

写Hook脚本时注意:服务器环境里的命令路径可能和交互式Shell不一样,必要时写全路径,比如/usr/bin/php而不是php,避免脚本执行时报command not found。

6.3 服务器端不鼓励使用的工作流

前面说过,服务器上的裸仓库没有工作目录,不能直接在服务器上编辑代码。有些新手会在服务器上clone一份普通仓库,然后直接在服务器上改文件再commit。这种操作方式在自建Git服务器里其实很别扭,容易造成代码和远程仓库不一致。我的建议是:服务器端只做存储和分支管理,所有代码改动都在本地完成。这样能最大程度避免权限问题、文件锁问题,也让仓库的数据更干净。

7. 仓库备份与日常维护

7.1 为什么自建仓库要格外重视备份

用Gitee、GitHub这类托管平台时,平台本身会做多副本存储,数据丢失概率极低。但自建服务器不同,一台机器挂了就是所有代码都丢了。所以备份这一步不能省略。

备份Git仓库有两种思路:一是直接备份裸仓库的目录文件,二是通过在服务器上定期clone来保存一份只读副本。我两种都会用,双保险。

7.2 定时备份脚本

创建备份脚本/usr/local/bin/git-backup.sh:

#!/bin/bash BACKUP_DIR="/srv/git-backup" DATE=$(date +%Y%m%d_%H%M%S) mkdir -p "$BACKUP_DIR" for repo in /srv/git/*.git; do name=$(basename "$repo") tar -czf "$BACKUP_DIR/${name}_${DATE}.tar.gz" -C /srv/git "$name" done # 保留最近7天的备份,更早的删除 find "$BACKUP_DIR" -name "*.tar.gz" -mtime +7 -delete

赋予执行权限,加入crontab:

sudo chmod +x /usr/local/bin/git-backup.sh sudo crontab -e

在crontab里加一行:

0 2 * * * /usr/local/bin/git-backup.sh

这样每天凌晨2点备份一次,保留7天。

7.3 仓库迁移

换服务器时,迁移仓库很简单。新服务器上创建好同样的/srv/git目录后,直接把旧的裸仓库目录打包拷贝过去,或者从旧服务器上clone一份裸仓库:

git clone --bare git@旧服务器IP:/srv/git/awesome-project.git

然后把生成的awesome-project.git目录移动到新服务器的/srv/git/下就行。开发者本地的remote地址会从旧IP指向新IP,只需要改一下remote:

git remote set-url origin git@新服务器IP:/srv/git/awesome-project.git

7.4 仓库体检与垃圾回收

Git仓库用久了,因为频繁的提交和分支操作,对象数据库里会有很多不可达对象。虽然不影响功能,但会占磁盘空间。可以定期执行:

sudo -u git git --git-dir=/srv/git/awesome-project.git gc --aggressive --prune=now

git gc会清理冗余对象并压缩存储。个人经验是不要在开发高峰期执行,因为比较吃CPU和IO,在凌晨配合crontab跑比较合适。

7.5 磁盘空间监控

一个容易被忽视的点是服务器磁盘空间。代码本身不大,但仓库的.git目录会随着历史提交不断膨胀,尤其是包含大文件的时候。我遇到过一次服务器磁盘写满导致所有push失败的教训,从那以后我在服务器上加了简单的空间监控:

df -h /srv/git

建议每周看一眼,或者在crontab里加一个磁盘使用率告警:

0 9 * * * df -h /srv/git | awk 'NR==2 {if ($5+0 > 80) print "磁盘空间告警", $5}' >> /var/log/git-disk.log

8. 常见问题排查:从连接失败到push冲突

8.1 搭建完成后本地无法连接

搭建好服务器后进行测试,常见问题按下面顺序排查:

  • 服务器SSH服务是否正常运行:systemctl status ssh。
  • 服务器防火墙是否放行22端口。Ubuntu默认的ufw如果启用了,需要执行sudo ufw allow 22/tcp。
  • 客户端是否能Ping通服务器。有些云服务器的安全组规则需要在控制台单独配置。
  • 使用的用户名和密钥是否正确。

8.2 push时提示"Everything up-to-date"但没有推送成功

这个提示其实是正常的,意思是本地没有新的提交需要推送。如果你确认本地有提交但提示还是这样,检查一下当前分支是否设置了上游:

git branch -vv

如果显示[origin/main]说明上游设置好了,如果显示[origin/main: gone]说明远程分支被删了或者未正确设置,重新设置一下:

git branch --set-upstream-to=origin/main main

8.3 push时报错"non-fast-forward"

当远程仓库有本地没有的新提交,而你强制推送时,Git会拒绝这个操作,提示non-fast-forward。这是保护机制,避免覆盖别人的提交。

正确做法是先拉取远程更新:

git pull --rebase git push

如果确定要覆盖远程历史(比如代码改错了想回退),可以用--force参数,但强烈建议不要在主分支上使用:

git push --force

现在Git 2.30以上版本还提供了--force-with-lease,这个参数会在远程分支和本地记录一致时才允许强制推送,更安全:

git push --force-with-lease

8.4 服务器重启后Git无法访问

这种情况基本是SSH服务没有设置开机自启。检查:

sudo systemctl enable ssh sudo systemctl start ssh

如果用的是阿里云、腾讯云这类云服务器,还要确认安全组里有没有放行端口。这些平台即使你服务器内部防火墙开着,安全组不给放行也进不来。

8.5 一个奇特的坑:服务器时间不准导致Git操作失败

这个问题比较冷门但遇到过。Git的提交、SSH握手都会依赖系统时间,如果服务器时间偏差太大,可能出现证书校验失败或者奇怪的握手错误。排查方法:

date

如果时间和实际时间差太多,安装ntpdate校准一下:

sudo apt install ntpdate -y sudo ntpdate ntp.aliyun.com

也可以直接用systemd-timesyncd同步时间:

sudo timedatectl set-ntp true

处理好之后Git仓库的push和clone就恢复正常了。

9. 给新手的几条实操建议

最后聊几个我实际使用下来觉得很有用的经验。

第一,写commit信息一定要认真。很多人刚开始觉得"能提交就行",结果项目做大了之后回看提交历史,全是fix、update、111这种,完全定位不了问题。我自己规定:commit message第一行不超过50个字符,简明扼要说清楚做了什么,如果要补充细节就空一行再写。这个习惯能让你在半年后依然看得懂自己当时改了什么。

第二,重视.gitignore。在项目一开始就创建.gitignore文件,把node_modules、target、.env这些不应该进仓库的目录和文件排除掉。不要等代码已经提交了再改,因为后面清理历史非常麻烦。我不止一次看到有人把数据库密码和密钥直接推到仓库里,最后被迫换密码的情况。

第三,大文件不要往普通Git仓库里塞。Git对二进制文件、视频、压缩包这些不友好,会让仓库体积迅速膨胀。如果团队确实需要存储大文件,可以考虑git-lfs扩展,或者把这些文件单独放到对象存储里。

第四,小团队的仓库管理可以简单,但分支策略要有。哪怕只有两个人协作,也建议约定好主干分支保持可发布状态,所有新功能开分支开发,合并前先review或用CI跑一遍测试。从项目第一天就养成这个习惯,后面协作效率会高很多。

我在Ubuntu服务器上搭裸仓库的经验就这样了。这套方案不复杂,但胜在稳定、轻量,运行几年也不会出大问题。如果你用下来感觉不够用,再往Gitea或GitLab走也来得及——至少基础的东西已经打好了。

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

AGX Orin载板设计避坑指南:电源时序、存储选型与高速接口布局

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

作者头像 李华
网站建设 2026/9/29 2:02:04

X-Bogus 不是加密:前端请求签名机制与复现路径

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

作者头像 李华
网站建设 2026/9/29 2:01:46

Vue3进阶实战:手写响应式内核、路由权限与Pinia工程化

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

作者头像 李华
网站建设 2026/9/29 2:01:44

实时机器视觉系统集成:高速相机选型与链路设计要点

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

作者头像 李华
网站建设 2026/9/29 2:01:17

智能车竞赛开源实录:硬件设计、图像处理与PID角速度控制

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

作者头像 李华