news 2026/10/16 10:36:08

Docker实战指南:从核心概念到容器化部署的完整入门教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Docker实战指南:从核心概念到容器化部署的完整入门教程

1. 为什么我最终把 Docker 放进了日常工具箱

两年前我第一次部署一个内部工具,花了整整一个下午。开发机上一个版本,测试服务器上另一个版本,生产环境又是半年前的老版本。光是"怎么把环境弄得和开发机一样"这个问题,就耗掉了半天。后来我用了 Docker,才理解了一件事:大多数人部署困难的根本原因,是把"应用"和"运行环境"混在一起管理了。

Docker 做的事情其实很朴实:它把应用代码、依赖库、系统配置、启动命令全部打包成一个标准化的"包裹",也就是镜像。任何一台装了 Docker 的机器,不管是 Windows、macOS 还是 Linux 服务器,都能把这个包裹解开,跑出一个几乎完全一致的环境。这种能力对你的价值可以这样理解:以前你交付的是源代码和一份"怎么安装环境"的说明文档,现在你交付的是一个"开箱即用"的完整系统。

这篇教程我不打算写成官方文档的中文翻译版,也不打算堆砌命令大全。我的目标是从一个实际用户的角度,带你走完"理解 Docker 核心概念、搭建环境、跑起第一个容器、写出自己的 Dockerfile、用 docker-compose 管理多容器项目"这条完整路径。内容里会穿插我踩过的坑、换过的方案、以及为什么我最终选择这样做而不是那样做。无论你是前端开发者想搞懂后端部署,还是后端工程师想规范交付流程,或者是学生想为简历加一项实用技能,这篇文章应该都能让你少走一些弯路。

2. 镜像、容器、数据卷:先把这个三角关系刻进脑子里

很多教程一上来就让你敲docker run hello-world,跑通了感觉挺神奇,但过两天一忘,等于白学。我建议你先花十分钟把 Docker 的三个核心概念搞明白,后面所有操作都会变得非常顺。

2.1 镜像:一个只读的"安装包"

镜像可以理解成一个压缩好的操作系统快照,里面包含了你应用运行所需的全部东西:基础操作系统文件、运行时环境、依赖包、配置文件、代码。它的关键属性是只读,也就是说你运行一个镜像时,不能直接修改镜像本身,只能基于它创建容器来运行。

镜像还有一个重要的特性——分层。Dockerfile 里的每一条指令都会生成一个层,这些层可以被多个镜像共享。比如你用的基础镜像如果和其他镜像相同,那它们可以复用本地已有的层,既省磁盘又省下载时间。这个机制也意味着:如果你只改了项目代码,重新构建时只需要重新打包代码那一层,剩下几层直接走缓存,速度非常快。

2.2 容器:镜像的"运行实例"

容器是从镜像启动的、可读写的进程。它有自己的文件系统、网络栈和进程空间,但和虚拟机不同的是,多个容器共享宿主机内核,所以资源开销极低。虚拟机模拟的是"一台完整的电脑",容器只是"一个隔离的进程"。

打个比方:如果说镜像是做蛋糕的模具,那容器就是用模具做出来的一个个蛋糕。同一个模具可以做很多个蛋糕,同一个镜像也可以启动很多个容器。每个容器都是独立运行的,你在一个容器里改文件、装软件、跑服务,不会影响其他容器,更不会污染宿主机。

2.3 数据卷:容器删除后数据还在的"外部硬盘"

有一个新手最常见的惨痛教训:容器是临时的,你把数据库跑在容器里,然后一个docker rm把容器删了,数据全没了。解决方案就是数据卷(Volume)。数据卷是独立于容器生命周期的存储空间,你可以在运行容器时把容器内的某个目录挂载到数据卷或宿主机目录上。这样就算容器删掉重建,只要挂载关系还在,数据就还在。

我用 Docker 管理 MySQL 和 PostgreSQL 的经验是:数据库类容器一定要挂载数据卷,否则哪天误删容器就是事故现场。后面的实操部分会专门演示数据卷的用法。

3. 从零开始:安装、镜像加速与第一个容器

在正式开始操作之前,先把环境搭好。这部分看起来简单,但我在不同操作系统上都踩过一些细节问题,写出来给你避避雷。

3.1 安装 Docker Engine 还是 Docker Desktop

Linux 服务器上,我建议直接安装 Docker Engine,它只包含最核心的守护进程和命令行工具,干净轻量。安装方式通常是用官方源,或者你用的 Linux 发行版自带的包管理器。

Windows 和 macOS 上则建议安装 Docker Desktop,它会帮你管好容器运行时、命令行工具、以及一个图形界面。Windows 上尤其要注意:安装前请确认 BIOS 里已经开启 CPU 虚拟化,安装过程中它一般会自动启用 WSL 2 后端。如果没有 WSL 2 而是用 Hyper-V,性能会有明显差别,建议优先用 WSL 2。

3.2 配置镜像源:国内环境绕不开的一步

Docker 默认从 Docker Hub 拉取镜像,国内直连往往慢得让人怀疑人生。这一步不是申请什么特殊服务,就是修改 Docker 的守护进程配置文件,把镜像源换成本地可访问的地址。常见的做法是编辑/etc/docker/daemon.json,Linux 上路径一般是这个,Docker Desktop 则在设置界面里找到 Docker Engine 的 JSON 配置区:

{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://dockerproxy.com", "https://docker.mirrors.ustc.edu.cn" ] }

改完以后执行sudo systemctl restart docker(Linux)或者在 Docker Desktop 里点 Apply & Restart。这里有个容易混淆的点:镜像源只管拉取镜像的速度,和容器运行时的网络没有关系。容器内部的网络默认走宿主机,访问外网不需要额外配置。

3.3 用 hello-world 和 nginx 验证环境

安装完成后,验证环境最直接的方式是跑一个最小容器:

docker run --rm hello-world

这个镜像非常小,而且设计上就是打印一段说明文字就退出。--rm参数表示容器一退出就自动删除,适合做这种一次性的验证操作。如果看到Hello from Docker!那段输出,说明你的 Docker 环境基本正常。

接着跑一个真正有用的服务,Nginx:

docker run -d --name web-test -p 8080:80 nginx:alpine

参数解释一下:-d是后台运行,--name给容器起一个名字,-p 8080:80表示把宿主机的 8080 端口映射到容器内的 80 端口。跑完后打开浏览器访问http://localhost:8080,如果看到 Nginx 的欢迎页,说明端口映射、网络转发这条链路都是通的。nginx:alpine是精简版 Nginx 镜像,体积比默认的nginx小很多,日常调试优先用这种标签。

4. 生命周期管理:容器不是启动就完了,你得会"养"

跑起来一个容器只是开始,实际使用中你会不断面对"容器挂了怎么办""怎么进去看日志""怎么更新镜像""怎么让容器开机自启"这些问题。这一节我把日常管理容器的核心操作串一遍。

4.1 查看、停止、删除:基本操作里的常见误区

docker ps # 查看运行中的容器 docker ps -a # 查看所有容器,包括已停止的 docker stop web-test docker start web-test docker rm web-test

很多新手会在这里踩两个坑。第一个坑是docker rm直接报错,提示容器还在运行,必须先 stop 再 rm,或者直接用docker rm -f强制删除。第二个坑是习惯了docker ps只看到运行中的容器,结果容器悄悄退出了却不知道,排查时一脸懵。要知道docker ps -a才能看到已退出的容器和它们的退出状态码。

查看日志也是高频操作:

docker logs -f --tail 100 web-test

-f是持续跟踪,--tail 100表示只输出最近 100 行。排查容器不正常退出时,先看日志基本能判断个八九不离十。

4.2 进入容器交互式排查:exec 与 attach 的区别

容器里的服务出问题时,我习惯先看日志,如果日志不够,就进入容器内部检查:

docker exec -it web-test /bin/sh

exec的含义是在运行中的容器里执行一条命令,-it是分配一个交互式终端。/bin/sh是进入容器的 shell。注意 Nginx 的 alpine 镜像里只有/bin/sh,没有 bash,所以别用bash -l去试。

这里要区分docker attach和docker exec。attach会把你的终端直接挂到容器的主进程上,看到的是主进程的输出,一按 Ctrl+C 很可能直接把容器停掉了。而exec是开启一个新进程,你退出 exec 不会影响容器本身。日常调试我用 exec 的频率远高于 attach,建议你也养成这个习惯。

4.3 容器开机自启:重启策略的合理配置

服务器重启后,你的容器能不能自动起来,取决于运行容器时是否指定了重启策略:

docker run -d --name web-test --restart unless-stopped -p 8080:80 nginx:alpine

重启策略有几种选择:no(默认,不自动重启)、on-failure(非正常退出时重启)、unless-stopped(除非手动停止,否则一直保持运行)、always(总是重启)。我的习惯是:除非你明确知道某个容器是一次性任务,否则都加上--restart unless-stopped。这样即使宿主机重启,服务也能自动恢复,不依赖人工介入。

如果容器已经创建了但没加重启策略,不用重建,执行docker update --restart unless-stopped web-test修改即可。

5. Dockerfile 编写:从"跑别人的镜像"到"构建自己的镜像"

如果说跑通 Nginx 让你入坑了 Docker,那学会写 Dockerfile 才是真正解锁 Docker 生产力的时刻。因为日常项目中你几乎不可能直接用现成的官方镜像,多少都要定制一下。

5.1 一个最简单的 Node.js 镜像实例

我用一个 Node.js 项目举例,从零写一个 Dockerfile。假设目录结构是:

my-app/ ├── package.json └── index.js

Dockerfile 第一版长这样:

FROM node:20-alpine WORKDIR /app COPY package.json ./ RUN npm install COPY . . CMD ["node", "index.js"]

逐行拆解:

  • FROM:指定基础镜像。node:20-alpine是基于 Alpine Linux 的精简 Node.js 镜像,体积小很多。
  • WORKDIR:设置工作目录,后面的操作都默认在这个目录下执行。如果目录不存在,Docker 会自动创建。
  • COPY package.json ./:把 package.json 复制进镜像,注意这里先只拷 package.json,而不是整个目录。
  • RUN npm install:安装依赖。因为依赖安装在镜像层里,这一层构建完成后会被缓存。
  • COPY . .:把项目所有文件复制进来。
  • CMD:容器启动时执行的命令,只能有一个,如果写了多个只有最后一个生效。

这套写法的用意是充分利用层缓存。你改的是index.js这种项目代码,只要package.json没变,npm install这一层就不会重新执行。如果你一上来就COPY . .再RUN npm install,那每次改变代码都会让依赖安装重新跑一遍,构建时间可能是几倍差距。

5.2 多阶段构建:为什么我推荐你直接养成这个习惯

前端项目构建场景最能体现多阶段构建的价值。我早先写过一个 Vue 项目,那时容器里既要有 Node 编译,又要保留源码文件,镜像动不动就 1GB 以上。后来我改成多阶段构建:

# 阶段一:构建 FROM node:20-alpine AS builder WORKDIR /app COPY package.json ./ RUN npm install COPY . . RUN npm run build # 阶段二:运行 FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]

这个 Dockerfile 里有两个FROM。第一阶段完成编译,第二阶段只要把第一阶段的构建产物dist目录拷贝进 Nginx 镜像。最终生效的是第二阶段,第一阶段的中间产物全部丢弃。这样最终镜像只有 Nginx 和静态文件,体积通常在几十 MB 级别。

daemon off;这个参数也值得说明一下。Nginx 官方镜像里,这个参数让 Nginx 以前台方式运行,因为 Docker 容器要求至少有一个前台进程,否则容器会立即退出。

5.3 构建镜像与修复问题的完整流程

写好 Dockerfile 后,执行构建:

docker build -t my-app:v1 .

-t给镜像打标签,格式是名称:标签,这里的.表示构建上下文是当前目录。构建上下文会整个发给 Docker 守护进程,所以记得在项目里加一个.dockerignore文件,把node_modules、.git、dist这种目录排除掉,否则构建时会变得很慢。

构建过程中最常见的报错是npm install失败,多半是网络原因。你可以在 Dockerfile 里换成国内 npm 镜像源:

RUN npm install --registry=https://registry.npmmirror.com

或者直接在基础镜像里设置环境变量:

ENV npm_config_registry=https://registry.npmmirror.com

构建完成后运行自己的镜像:

docker run -d --name my-app -p 3000:3000 my-app:v1

如果端口、路径、权限有问题,先docker logs看输出,再docker exec进去手动执行命令验证。这个"构建—运行—查日志—修复 Dockerfile—重建"的循环,你用几次就会非常熟练。

6. 端口映射与跨容器通信:多服务联调的正确姿势

你已经能跑起一个服务了,但实际项目通常是"前端 + 后端 + 数据库"三件套。要让它们协同工作,涉及端口映射和容器间通信两个核心问题。

6.1 为什么容器之间要避免用 localhost 通信

我最早犯过一个错误:后端容器里配置数据库连接,地址写成localhost:3306,结果怎么连都连不上。原因其实很清楚:每个容器有自己独立的网络命名空间,容器里的localhost是容器自身,不是宿主机,也不是其他容器。

容器之间的正确通信方式是用容器名。比如你启动了数据库容器docker run --name mysql-server -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:8,那么后端容器连接它的地址应该是mysql-server:3306。前提是两个容器在同一个 Docker 网络里。

6.2 创建自定义网络:比默认 bridge 更可靠

Docker 安装时会自带一个默认网络,但多个容器直接都跑在默认网络上,容器名解析有时会有问题。我的做法是显式创建一个网络:

docker network create app-network

然后在启动容器时都加上:

docker run -d --name backend --network app-network -p 3000:3000 my-app:v1 docker run -d --name mysql-server --network app-network -e MYSQL_ROOT_PASSWORD=my-secret-pw -p 3306:3306 mysql:8

这样backend容器里就能通过mysql-server这个名字访问数据库容器了。注意-p 3306:3306只是把数据库端口暴露给宿主机,方便你在宿主机上用数据库客户端工具连接调试,容器之间的通信走网络内部,不需要经过端口映射。

6.3 端口冲突与占用排查经验

端口冲突是我被问得最多的问题之一。docker run -p 8080:80时报错port is already allocated,通常是宿主机上已经有进程占用 8080 了。排查手段很直接:

lsof -i :8080

或者用:

netstat -tulpn | grep 8080

查出来是哪个进程占用,要么停掉它,要么换一个宿主机端口。还有个容易被忽略的点:-p 8080:80和-p 80:8080方向不同,前者是把宿主机 8080 转发到容器 80,后者反过来。写反的时候,容器里的服务监听的是 80,你却把宿主机 8080 暴露成了 80,访问自然不对。

7. docker-compose:一键拉起整套项目的编排利器

当你开始面对"数据库 + 后端 + 前端 + 缓存"这种多容器协同的场景,再用一条条docker run命令去管理会非常痛苦。docker-compose 就是用来解决这个问题的:把一个项目的所有容器定义在一个 YAML 文件里,一条命令全部启动。

7.1 一个典型项目的 compose 文件长什么样

以下面这个组合为例:一个 Node 后端、一个 MySQL 数据库、一个 phpMyAdmin 管理工具。

version: "3.8" services: backend: build: ./backend ports: - "3000:3000" environment: DB_HOST: mysql-server DB_USER: root DB_PASSWORD: my-secret-pw depends_on: - mysql-server mysql-server: image: mysql:8 environment: MYSQL_ROOT_PASSWORD: my-secret-pw volumes: - mysql-data:/var/lib/mysql restart: unless-stopped phpmyadmin: image: phpmyadmin/phpmyadmin ports: - "8080:80" environment: PMA_HOST: mysql-server volumes: mysql-data:

启动方式只有一行:

docker-compose up -d

-d同样是后台运行。docker-compose 会自动创建一个专属于这个项目的网络,所有 service 之间可以通过 service 名字互相访问,所以后端里配置DB_HOST: mysql-server就能连通数据库。

7.2 depends_on 的局限与健康检查

depends_on控制容器的启动顺序,但它只保证"数据库容器启动了",不保证"MySQL 已经准备好接受连接"。如果你的后端启动比数据库初始化快,可能会连不上数据库而崩溃。

我的解决办法是加健康检查:

mysql-server: image: mysql:8 environment: MYSQL_ROOT_PASSWORD: my-secret-pw healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 5s timeout: 3s retries: 10 volumes: - mysql-data:/var/lib/mysql

然后后端的depends_on改成:

depends_on: mysql-server: condition: service_healthy

这样后端容器会在数据库健康检查通过后才启动,从根源上消掉了"容器起来了但服务连不上"的随机性问题。

7.3 查看状态与清理环境

docker-compose ps # 查看所有服务状态 docker-compose logs -f backend # 跟踪某个服务的日志 docker-compose down # 停止并移除所有容器和网络

down后面加-v会同时删除数据卷,这招慎用,尤其是数据库数据。我自己的习惯是平时只用down不带-v,确认要彻底清理数据时再单独操作。

8. 我在实际项目中踩过的坑,希望你别再踩一遍

这部分内容是纯经验分享,每一条都是我在真实项目里遇到过的问题。如果你能把它们记下来,能省下大量排查时间。

8.1 时区问题:容器里的时间默认是 UTC

默认情况下,容器内的时间是 UTC 时区,和北京时间相差 8 小时。如果项目里有定时任务、日志时间戳、或者对时间敏感的业务逻辑,这一点会让你莫名其妙地踩坑。

解决方法是设置环境变量:

environment: - TZ=Asia/Shanghai

如果是 Dockerfile,可以加上:

ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

但要注意的是,有些镜像的基础系统里可能没有tzdata这个包,需要先安装。我在 Alpine 镜像上就遇到过ln -snf失败的情况,执行前先apk add --no-cache tzdata就好。

8.2 时区之外还有一个隐藏坑:容器内用户权限

很多官方镜像默认以 root 用户运行,方便是方便,但生产环境里这样做有安全隐患。我在一个项目里遇到过容器内写的文件属于 root,宿主机普通用户删不掉的情况。

解决思路是在 Dockerfile 里创建专用用户:

RUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser

这么做之后,如果应用要写某个目录,记得先chown给这个用户。在涉及数据库数据目录时尤其要注意,否则容器启动时会报权限错误。

8.3 镜像体积膨胀:别急着加依赖,先看层缓存

镜像动不动几个 GB,基本是两个原因。一是没用多阶段构建,把构建工具留在了运行环境里;二是每一条RUN命令都会产生一个镜像层,如果中间层里堆了很多临时文件,就算后面删掉了,镜像体积也已经膨胀了。

把多条操作合并成一条RUN可以有效减少层数量。比如:

RUN apk add --no-cache tzdata \ && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ && echo $TZ > /etc/timezone

用&&连接,最终只生成一个镜像层。这是书写 Dockerfile 的一个重要习惯。

8.4 Linux 服务器上 WSL2/Hyper-V/containerd 的环境差异

如果你在 Windows 上用 Docker Desktop,运行时的后端可以是 WSL 2 也可以是 Hyper-V,两者性能差距在磁盘 IO 密集型任务上非常明显。WSL 2 的启动速度和文件访问性能都更优,建议优先选择 WSL 2。

Linux 服务器上还有一种情况:使用 containerd 作为容器运行时相比默认的 runc 模式,在某些场景下更稳定。但对入门用户来说,这一步不用太纠结,先把 Docker 默认配置用熟,再根据实际需求去研究运行时差异。

9. 从会用到用熟:几个值得长期养成的习惯

你如果已经跟着上文实操了一遍,基本上已经能独立管理容器化的服务了。最后分享几个我坚持了很久的习惯,它们帮助我在各种项目中少踩了很多坑。

第一个习惯是每次运行容器都加上--name。没有名字的容器会用随机 ID 标识,虽然也可以操作,但记忆成本高很多。给容器起个有意义的名字,后面docker logs、docker exec、docker stop都顺手很多。这个习惯同样适用于 compose 文件里每个 service 的命名。

第二个习惯是数据库容器必须挂载数据卷并做好命名。我见过太多人跑起 MySQL 容器后只用默认匿名卷,容器一删数据就找不回来。给数据卷起一个和业务相关的名字,比如mysql-data,后续备份和迁移都能少走弯路。

第三个习惯是给镜像打标签时带上版本号。不要只打latest标签,生产环境最好固定到具体版本,比如my-app:v1.2.3。这样出问题时你可以快速回滚到上一个镜像,而不是重新构建一个不知道内容是什么的"最新版本"。

第四个习惯是关于环境变量的管理。不要在 Dockerfile 或 compose 文件里硬编码密码。docker-compose 里可以使用.env文件:

environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}

然后项目目录下放一个.env文件,用MYSQL_ROOT_PASSWORD=yourpassword这样的格式填入真实值。这样密钥不会随镜像分发,也不会误提交到代码仓库。

最后一个习惯是定期清理无用资源:

docker system prune

这个命令会清理停止的容器、无用的网络、悬空镜像和构建缓存。加-a会连不被任何容器引用的镜像一并清理,建议你确认无误再执行。我在本地开发机上基本每周跑一次,磁盘占用能少很多。

写到这里,从理解容器和镜像的关系,到跑通第一个服务,再到用 compose 管理整套项目,你已经拥有了日常使用 Docker 所需的完整知识拼图。剩下的就是多练。建议你拿一个现有项目动手改造,哪怕只是加一个 Dockerfile 和 compose 文件,比看十篇教程都要管用。

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

CAN总线基础详解:从差分信号到非破坏性仲裁,嵌入式必学

做嵌入式的,尤其是跟汽车电子沾边的,几乎没人能绕开CAN这门课。不管你是刚拿着开发板点灯的新手,还是被总线上一堆莫名其妙的Error Frame折磨过的老手,CAN基础都是必须夯实的地基。这篇是“CAN基础内容详解”系列的第一篇&#xf…

作者头像 李华
网站建设 2026/10/16 10:35:36

零成本搭建Claude AI辅助编程环境:分层组合方案与实操指南

1. 这个项目到底在解决什么问题 第一次看到“free-claude-code”这个标题,很多人会误以为这是某个破解工具或者灰色地带的产物。实际上,它代表的是一类非常务实的工程实践: 如何在不依赖付费订阅的前提下,让 Claude 这类大语言模…

作者头像 李华
网站建设 2026/10/16 10:35:16

内容安全与合规审核:守护清朗网络的关键实践

简介:这是一份NTRMAN团队出品的《迷失的季节 Seasons of Loss》V0.4R3版完整游戏资源包,面向喜爱剧情驱动型叙事游戏的玩家,也适合需要分析Unity游戏结构的开发学习者。压缩包采用zip格式,内部共143个文件,类型以dll动…

作者头像 李华
网站建设 2026/10/16 10:33:54

人人都用但用不明白的CRC32:嵌入式避坑与实战解析

做嵌入式这几年,我越来越觉得CRC32是一个“人人都用,但没几个人用明白”的东西。去年帮一个做测量仪表的团队救过一次现场:设备OTA升级包在传输环节被改坏了一个字节,Bootloader校验CRC32居然通过了,固件跑起来之后间歇…

作者头像 李华
网站建设 2026/10/16 10:33:13

取牌游戏:用扑克牌具象化理解队列FIFO本质

1. 为什么一个“取牌游戏”能讲透队列的本质?你有没有试过这样教学生理解队列:画个箭头,左边写“入队”,右边写“出队”,再强调一句“先进先出”——然后看着他们点头,但一到写代码就卡在queue.pop(0)和que…

作者头像 李华
网站建设 2026/10/16 10:31:01

中望3D工程图三维点坐标统计表:批量坐标提取与标注实战

机械加工里,有一种最让人头疼的图纸:不是公差标得严的那种,而是密密麻麻几十个孔位、检测点堆在一起,光靠尺寸标注根本说不清楚。比如薄壁板件上按阵列分布的几十个孔径取样点,或者铸件毛坯需要记录变形量的测量点。过…

作者头像 李华