Jenkins Pipeline声明式语法详解:从零构建自动化流水线

发布时间:2026/10/17 10:47:26
Jenkins Pipeline声明式语法详解:从零构建自动化流水线 用了这么多年的持续集成我的体会是Pipeline 语法就是把零散的构建部署命令整理成一条可控制、可观察、可复用的自动化流水线。它不复杂但跟写脚本不一样——Pipeline 有自己的结构、作用域和状态管理方式真正用顺手需要一个熟练的过程。这也是我想写这篇详解的原因把声明式语法拆开、揉碎一条一条讲清楚给我的实践记录留个底也让刚上手 Jenkins Pipeline 的朋友省掉踩坑的时间。1. 先把两种语法分清楚声明式与脚本式Pipeline 本身有两种写法声明式Declarative和脚本式Scripted。两者底层是同一套执行引擎但表达方式完全不同。1.1 声明式为什么是默认选择声明式语法看起来像配置而不是代码。它的特点是结构固定按pipeline、stages、steps这样几个大块去组织每一层都有明确的指令。一个最小可运行的声明式流水线是下面这个样子pipeline { agent any stages { stage(Build) { steps { echo 正在构建... } } } }对于大多数项目场景声明式完全够用而且优势很明显结构规整团队里谁看都能快速摸清整个流程它自带很多语法约束写错了会在解析阶段直接报错不容易出现运行时才崩的情况。1.2 什么时候得用脚本式脚本式基于 Groovy 语法本质是一段跑在 Jenkins 上的脚本。它的特点是灵活可以写for循环、try/catch、自定义函数还可以自由赋值。下面是一段典型脚本式流水线node { stage(准备) { def branch env.BRANCH_NAME ?: master echo 当前分支: ${branch} } stage(构建) { sh make build } }如果你只是在几个stage里顺序执行几条命令完全没必要用脚本式。但如果你的流程里存在复杂的条件判断、循环遍历多个模块或者抽象出公共函数给多个流水线复用脚本式的灵活性才能体现出来。我的实际做法是外部框架用声明式只有局部逻辑确实绕不过去时才用script块把脚本式包进去。1.3 两种语法的边界与转换思路在声明式里steps块只能写声明式支持的指令比如sh、echo、git但你可以通过script块塞入任意 Groovy 代码。例如stage(动态处理) { steps { script { def items [a, b, c] items.each { item - echo 处理 $item } } } }这样做的好处是保持了声明式的主体结构同时获得了脚本式的能力。日常开发中哪些内容适合声明式哪些内容适合脚本式打个比方声明式是公交线路图每一步都固定脚本式是私家车想怎么开怎么开。项目里绝大多数流程就像公交线路所以先把声明式用扎实了。2. 声明式 Pipeline 的核心结构逐段拆解整个声明式语法可以理解为几层嵌套的抽屉最外层是pipeline里面放agent、stages、post其中stages又包含多个stage。2.1 pipeline 块的骨架结构一个典型的多阶段流水线结构长这样pipeline { agent none options { timestamps() timeout(time: 1, unit: HOURS) } environment { APP_ENV prod } stages { stage(拉取代码) { agent { label build-slave } steps { checkout scm } } stage(测试) { steps { sh ./gradlew test } } } post { always { junit build/test-results/**/*.xml } failure { slackSend channel: #ci-alert, message: 流水线失败: ${env.BUILD_URL} } } }注意agent none写在顶层意味着每个stage需要自己声明 agent。这在多节点流水线里非常常见比如拉代码和测试在一台机器上跑构建发布到另一台机器上跑。如果顶层已经写了agent any那么每个stage里再写agent就会覆盖掉全局配置相当于这个阶段单独换执行环境。2.2 agent工作在哪里执行agent指令负责指定流水线或某个 stage 在哪个执行节点上运行常用写法有以下几种agent any任意可用节点执行适合简单任务。agent none顶层不定义交给每个 stage 单独指定。agent { label docker-host }使用带有特定标签的节点。agent { docker { image node:20 } }在指定节点上启动一个容器来执行适合隔离依赖环境。agent { dockerfile { filename Dockerfile.build } }用项目里的 Dockerfile 构建临时镜像来执行。实际经验中不要图省事全局agent any。如果某个 stage 需要 JDK 17另一个 stage 只需要 Node 环境一个 agent 解决不了全部。正确做法是不同阶段使用不同标签的节点或不同容器确保每个阶段的环境是明确且最小的。另一个容易踩坑的地方是容器化 agent 的目录挂载。默认工作空间会挂载到容器里但如果你改了 Jenkins 节点的workspace路径部分容器配置可能找不到工作目录。我处理这类问题时会明确指定args -v /cache:/cache之类的参数把缓存目录挂进去避免每次构建都拉全量依赖。2.3 stages 与 steps任务怎么编排stages是流水线的主干里面每个stage表示一个阶段stage内必须包含steps除非内部还有嵌套 stages。steps支持多个步骤按顺序执行。stage(构建) { steps { sh npm ci sh npm run build sh tar -czf dist.tar.gz dist/ } }步骤之间默认是顺序执行的但相互之间也可以增加条件控制。如果某个步骤失败默认行为是直接中断流水线后续步骤不会执行。这是 Pipeline 和普通 Shell 脚本很大的不同——普通脚本如果不加set -e一条命令失败后还会继续往下跑而 Pipeline 默认就是 fail-fast。steps块内还支持嵌套逻辑最常见的是script块和withEnv临时指定环境变量steps { script { if (env.BRANCH_NAME main) { echo 主干分支走正式发布流程 } else { echo 分支构建跳过发布 } } withEnv([NODE_ENVproduction]) { sh npm run build } }2.4 stage 级扩展when、environment、tools、options一个完整的 stage 远不止steps这么简单常见扩展指令包括when控制该 stage 何时执行可以按分支、环境变量、表达式等条件判断。environment在该 stage 内生效的环境变量优先级高于顶层同名变量。tools指定需要自动安装的工具如 JDK、Maven。options针对整个流水线的全局选项也可以写在 stage 内部。when是最常用的控制条件下面几个例子覆盖了典型场景stage(部署) { when { branch main } steps { sh deploy.sh } } stage(仅标签触发) { when { tag pattern: v.*, comparator: REGEXP } steps { sh upload-package.sh } } stage(多条件判断) { when { allOf { branch develop environment name: ENABLE_DEPLOY, value: true } } steps { sh deploy-dev.sh } } stage(按表达式控制) { when { expression { return params.RUN_SLOW_TEST true } } steps { sh run-slow-tests.sh } }environment变量还有一层细节环境变量定义的位置不同生效范围完全不同。写在pipeline顶层整个流水线都能读写在 stage 内部只有那个 stage 能用。注意如果你的环境变量名跟 Jenkins 自带环境变量重名局部定义会覆盖系统值。我见过有人直接定义PATH结果把 Jenkins 的默认路径弄丢了后续命令全部找不到。所以自建变量尽量用带有特定前缀的名字避免覆盖系统环境变量。options的常用配置包括options { disableConcurrentBuilds() timeout(time: 30, unit: MINUTES) retry(3) timestamps() }disableConcurrentBuilds()禁止同一个任务并发执行对发布类任务很有用。否则两个提交同时触发流水线发布操作可能冲突。timeout限制整体执行时间单位可以是SECONDS、MINUTES、HOURS。retry在流水线失败后自动重试适合依赖网络波动的场景。timestamps()会让控制台日志每一行前面带上时间戳排查耗时问题非常好用。3. 连接外部世界的语法参数、凭据、触发器与共享库一个自动化流水线如果没有参数输入、凭据管理、外部触发这些能力就只能算半成品。这些是让流水线真正融入项目流程的关键。3.1 parameters把运行时的变量交给用户在 Jenkins 页面点击“构建”时弹出的参数表单就是通过parameters指令定义的。常用类型有string、text、booleanParam、choice、passwordpipeline { agent any parameters { string(name: BRANCH, defaultValue: main, description: 要构建的分支) choice(name: ENV, choices: [dev, staging, prod], description: 部署环境) booleanParam(name: WITH_TEST, defaultValue: true, description: 是否执行测试) password(name: DEPLOY_TOKEN, defaultValue: , description: 部署令牌) } stages { stage(參數校验) { steps { echo 分支: ${params.BRANCH} echo 环境: ${params.ENV} echo 是否测试: ${params.WITH_TEST} } } } }参数通过params对象读取。注意脚本式代码里也可以用但声明式里更推荐统一通过params.XXX引用避免直接读env.XXX造成类型和空值的混乱。布尔参数在when条件里使用时要留意类型可以写成expression { return params.WITH_TEST }它会自动转布尔判断。参数默认值真正生效的时机是触发构建时用户没有修改表单值。所以参数并不是代码里定义好流水线内部就能立刻读到的——首次定义参数后需要让构建至少自动触发一次或者手动点击“构建”生成参数表单后续才会稳定使用这个参数列表。3.2 credentials不要把密码写进 JenkinsfilePipeline 中最容易踩雷的是凭据管理。系统中凭据的种类很多用户名密码、SSH Key、Secret text、证书。凭据的 ID 是在 Jenkins 凭据管理里创建时指定的在流水线中引用的是这个 ID而不是密钥内容本身。比如一个 Secret text 凭据在流水线里这样用stage(调用接口) { steps { withCredentials([string(credentialsId: api-token, variable: TOKEN)]) { sh curl -H Authorization: Bearer $TOKEN https://api.example.com/deploy } } }如果是用户名密码形式的凭据withCredentials([usernamePassword(credentialsId: registry-cred, usernameVariable: REG_USER, passwordVariable: REG_PASS)]) { sh docker login --username$REG_USER --password$REG_PASS registry.example.com }这里有几个细节值得强调。第一withCredentials块内部凭据是以环境变量形式注入的所以 Shell 里用$TOKEN能取到但如果同一段sh里想通过echo打印凭据内容来调试Jenkins 会自动做日志脱敏这是内置好的一项安全保护。第二尽量不要把凭据注入到全局 environment 里。全局注入意味着整个流水线的所有步骤都能拿到这个值一旦某个步骤打印环境变量泄露面会变大。正确做法是尽量缩小范围在真正需要使用的 stage 内局部注入。第三不要把凭据 ID 当作秘密本身。凭据 ID 只是引用路径它本身出现在 Jenkinsfile 里是可以接受的一般不需要单独加密。3.3 triggers定时与轮询Pipeline 支持通过triggers定义触发方式常见的有cron定时触发和pollSCM定期检查代码变更后触发pipeline { agent any triggers { cron(H 2 * * *) pollSCM(H/5 * * * *) } }注意cron里用H表示哈希生成的值这是为了把不同任务的定时分散开避免同一时刻所有任务同时启动造成节点负载尖刺。如果你有两个流水线都写成0 2 * * *它们就会在凌晨两点整同时执行写成H 2 * * *Jenkins 会各自确定一个在两点那一个小时内的随机时间点这样更均匀。还有一种方式是 Webhook 触发。代码仓库在 push 或 merge 时向 Jenkins 发送请求触发对应分支的流水线。这个通常依赖插件配置Jenkinsfile 内部只需要保证checkout scm时正确检出当前触发对应的 commit 即可。实际使用中PR 事件触发的流水线和 push 事件触发的流水线建议用不同的任务模板或者 stage 条件区分不要全都跑同一套全量流程否则每次 PR 都要构建、测试、发布谁受得了。3.4 共享库与语法抽象当你在多个仓库里维护几套流水线很快就会发现大部分步骤都是重复的。比如所有后端项目的流水线都是“拉代码、编译、测试、上传制品、部署”。复制粘贴不是长久之计改一个步骤就要去所有仓库里同步更新。这时就需要共享库。共享库可以把重复逻辑封装成“自定义步骤”或者全局变量我常用的方式是在vars目录下建一个 Groovy 文件比如vars/buildJar.groovydef call(String module ) { sh java -jar build-tool.jar ${module} }然后在 Jenkinsfile 里直接调用buildJar(user-service)整个调用过程看起来就像使用内置步骤一样。注意共享库文件里定义的call方法是整个文件被调用时的入口方法名必须和文件名一致这点新手非常容易遗漏。共享库的使用需要在 Jenkins 系统配置里设置库名称、仓库地址和版本通常是一个分支名或 tag。在 Jenkinsfile 中引入Library(my-pipeline-lib) _ pipeline { agent any stages { stage(构建) { steps { buildJar(user-service) } } } }共享库的src目录下还可以放 Groovy 类用于封装更复杂的工具逻辑。我的体会是共享库的抽象边界要控制好通用到所有项目都能用的逻辑才放进去业务相关、一次性逻辑就留在仓库自己的 Jenkinsfile 里。过度抽象会让流水线本身变得不可读反而失去了声明式语法“一看就懂”的优势。4. 错误处理与后置动作post、重试与通知构建失败是常态真正决定一套流水线是否好用看的是它在失败时的表现能否快速定位原因、能否自动恢复、能否及时通知到人。Pipeline 的错误处理主要靠post条件和retry、timeout等细节控制。4.1 post 条件块的基本写法post位于流水线或 stage 内部根据流水线或 stage 的最终状态执行对应动作。状态包括always、success、failure、unstable、changed、aborted等pipeline { agent any stages { stage(构建) { steps { sh make build } post { success { echo 构建成功 } failure { echo 构建失败 } } } } post { always { archiveArtifacts artifacts: dist/*.tar.gz, allowEmptyArchive: true junit build/test-results/**/*.xml } failure { echo 流水线整体失败请查看日志 } success { echo 流水线整体成功 } changed { echo 本次构建结果和上一次不同 } } }always是最常用的块相当于finally无论成败都会执行。整理制品、归档测试报告这类操作放这里最合适。failure和success按结果分别处理。changed只有在状态和上一次构建不一致时才触发比如上次失败这次成功非常适合用来发“已经恢复”的通知减少告警疲劳。4.2 超时、重试与节点恢复有些操作天生不稳定比如依赖拉取超时、远程服务偶发失败。Pipeline 提供几个指令做局部保护stage(依赖安装) { steps { retry(3) { sh mvn dependency:go-offline } } } stage(构建) { options { timeout(time: 10, unit: MINUTES) } steps { sh make build } }retry(3)表示块内步骤最多尝试 3 次适合那些“重启一下就好”的偶发问题。timeout放在 stage 的options里限制这个阶段最久执行时间超时后终止并标记失败。注意retry是整体重试也就是说如果sh mvn dependency:go-offline执行到一半失败整个命令重新执行一次。重试的间隔时间没有办法直接在语法里配置如果业务上需要退避等待只能在脚本里手动sleep或者用外部脚本实现。这个限制在写高并发重试逻辑时需要注意。4.3 通知集成与失败现场保存处理失败不只是把状态标记出来更重要的是把失败现场保存下来。比如构建失败时unstable和failure之间的差别在于测试有失败案例但流水线没中断这时状态是unstable构建或部署命令失败状态是failure。两者在post里的处理策略经常不同前者可能只需要记录报告后者可能需要紧急介入。保存失败现场有几个常用手段post { failure { archiveArtifacts artifacts: build/logs/**/*.log echo 失败时间: ${currentBuild.startTimeInMillis} echo 构建详情: ${env.BUILD_URL} } }在通知类操作里有两种常见实践一种是把失败信息发到即时通信群另一种是把失败日志归档成一个可下载的工件。它们各有适用场景。但要注意不要把通知逻辑写得太啰嗦。失败后既要归档日志又要发消息还要调用外部系统记录工单如果这些环节其中一环再出问题反而掩盖了原始故障。最好保持通知通道精简一两个入口就够信息集中方便快速追查。5. 并行执行与性能parallel、stash/unstash、matrix流水线自动化之后下一步就是提速。多个阶段没有依赖关系时顺序执行就是浪费资源。Pipeline 的parallel指令让多个分支同时跑stash/unstash负责在分支间传递文件。5.1 parallel 并行 stage 写法在一个stage内声明parallel会让 Jenkins 把内部分支并发执行。常见写法stage(多平台测试) { parallel { stage(Linux 测试) { agent { label linux-node } steps { sh ./run-tests.sh linux } } stage(Windows 测试) { agent { label windows-node } steps { bat run-tests.bat } } } }如果并行分支跑在同一台节点上它们会共享这台机器的 CPU 和内存构建密集型任务反而可能互相拖慢。更合理的做法是让并行分支跑在不同的节点或容器里或者控制并发数为一个可管理的范围比如failFast true表示某一分支失败就立即终止其它分支stage(并行测试) { failFast true parallel { stage(单元测试) { steps { sh run-unit-tests.sh } } stage(集成测试) { steps { sh run-integration-tests.sh } } } }5.2 stash 与 unstash跨节点传文件并行分支里每个分支可能跑在不同的工作目录甚至不同的节点上。某个分支生成的文件另一个分支如果要使用直接按相对路径找文件大概率找不到。Pipeline 处理这个问题的标准做法是stash/unstash。stage(构建产物) { steps { sh make dist stash name: dist-files, includes: dist/** } } stage(部署) { steps { unstash dist-files sh deploy.sh } }stash相当于把文件打包存到 Jenkins master 上unstash再在当前工作区解包出来。注意stash有大小限制默认单文件不超过一定大小也不要无节制地下整个node_modules或包含海量缓存的目录进去否则每次 stash/unstash 都是在制造不必要的网络传输。常用实践是只 stash 真正需要的产物目录比如dist/、target/*.jar、build/libs/*.jar。另外一点同一时间多个并发分支stash时名字要错开不要两个分支都用同一个name否则后写入的会覆盖先写入的出现数据错乱。5.3 matrix一个 stage 跑多种组合当需要在一个 stage 里跑不同操作系统、不同版本的组合时手写parallel会显得很啰嗦matrix是更合适的语法。它让你声明一个“笛卡尔积”或自定义组合每个组合自动生成一个并行子任务stage(兼容性测试) { matrix { axes { axis { name OS values linux, windows } axis { name JAVA_VERSION values 17, 21 } } excludes { exclude { axis { name OS values windows } axis { name JAVA_VERSION values 17 } } } agent { label ${OS}-node } stages { stage(测试) { steps { echo 在 ${OS} 上使用 Java ${JAVA_VERSION} 测试 } } } } }上面这段会生成linux-17、linux-21、windows-21三个组合并行执行因为windows-17被excludes排除了。matrix里的agent可以用组合变量动态指定节点标签很适合多平台多版本兼容性测试场景。6. 从语法到落地调试方法与常见坑语法学会了是一回事真正跑起来又是另一回事。Pipeline 的错误信息很多是 Groovy 解析层面的读起来并不友好。这里分享我的调试路径和几个高频问题的排查思路。6.1 高频错误不是语法写错那么简单第一个高频错误是方法不存在或拼写错误。声明式 Pipeline 的steps里只能使用已安装插件提供的步骤。比如在没有安装某个插件的 Jenkins 实例上调用slackSend即使语法完全正确也会在流水线解析阶段直接报错。这类错误排查起来很快看一眼控制台顶部“Started by ...”之后的第一段报错信息基本就能定位。第二个高频错误是Groovy 表达式的引号转义问题。sh步骤里如果命令本身包含单引号、双引号甚至$符号很容易被外层 Groovy 字符串拦截。比如想执行一个带$VAR的 Shell 命令在 Groovy 双引号字符串里$VAR会被解析成 Groovy 变量而不是 Shell 变量。正确处理方式是Shell 内部变量用单引号包住整个命令或者用\$VAR转义sh echo $HOME // 传给 Shell正确Shell 变量 sh echo ${env.BUILD_NUMBER} // 先解析 Groovy 变量再传给 Shell正确 sh echo \$PATH // 需要保留 $PATH 时要转义第三个高频错误是在声明式外层混用脚本逻辑。比如把def变量直接写在stages外层没有包进script块里解析会直接失败。声明式只接受已知指令自定义变量、方法、循环都必须放进script块。6.2 调试技巧按需重跑与日志定位Pipeline 最有用的调试功能之一是Replay重放。在构建历史里选中某次构建点击 Replay可以直接修改本次构建用到的 Jenkinsfile 内容包括共享库外的管道代码然后重新运行。这意味着你不需要为了改一行业务逻辑就开一次新构建。我在实际开发中经常先用 Replay 微调参数验证确认无误后再把最终版的 Jenkinsfile 提交到代码仓库。另一个实用的工具是“从某阶段开始重跑”。如果流水线有 5 个阶段前 4 个都成功了只有第 5 个因为临时网络抖动失败不需要整条重跑。重启时可以选择从失败的阶段开始跳过前面的成功阶段。这在大构建或长时间测试场景下非常节省时间。日志定位的经验也有讲究不要直接翻全部日志重点关注[Pipeline]标记的行。Jenkins 的日志输出里有没有[Pipeline]前缀能帮你区分哪些是执行框架的提示信息哪些是应用本身打印的内容。出现超时或失败时先看最后 50 行到 100 行的日志大部分问题都集中在尾部。// 在流水线里输出变量时建议统一走日志不要用 echo 到文件再手动翻 script { println BRANCH_NAME${env.BRANCH_NAME} println GIT_COMMIT${env.GIT_COMMIT} }6.3 几个真实场景的踩坑记录下面几条是我自己在多个项目反复踩过的坑整理成速查表供参考场景现象原因解决方案多分支构建时找不到目标分支代码工作区里检出的是上次构建的代码没有正确执行checkout scm或首次创建的分支未触发对应扫描在第一个 stage 显式执行checkout scm确认分支插件扫描周期配置合理sh里执行命令失败但流水线仍显示成功用户在sh末尾加了 true环境变量未生效sh里引用变量得到空值变量作用域不对或拼写与定义时不一致用env.XXX显式读取避免在不同块中直接引用裸变量名并发构建互相干扰两个任务同时构建时产物串了多个构建在同一工作区运行未做隔离加options { disableConcurrentBuilds() }或在节点上按分支区分工作目录凭据变量忘了解脱日志打印****但 Shell 里还是看到明文withCredentials范围外直接用了全局注入值缩小凭据注入范围并且绝不在environment里注入凭据后打印整份环境变量还有一条经验不要在steps里写过于复杂的 Shell 命令。如果一条命令超过 10 行我建议直接抽成一个 Shell 脚本放进项目仓库Pipeline 里只调用。这样便于本地复现、调试也让 Jenkinsfile 保持简洁可读。7. 流水线的演进从能跑到跑得好关于 Pipeline 语法我的实际体会是它更像一门“组织管理”的语法重点在于划分任务边界和流转条件而不是替脚本语言写逻辑。真正跑顺一套流水线之后下一步通常不是再写更多语法而是做减法把重复的、过时的阶段删掉把临时手动的操作逐步纳入流水线把真正需要人判断的时刻保留为人工确认步骤。人工确认是input指令的典型使用场景。与其让每个人都直接在页面上触发生产发布不如在关键阶段加一个等待批准的交互节点stage(等待生产审批) { input { message 确认发布到生产环境 ok 确认发布 parameters { string(name: RELEASE_NOTE, defaultValue: , description: 发布说明) } } steps { sh ./deploy.sh --prod } }input会让流水线暂停等待有权限的用户在页面上点击确认或填写参数后再继续。这套机制比单纯在 Jenkins 外部建审批流程更直接因为审批人和审批上下文都集中在一个界面上不会把流程切得七零八落。写在最后从最简的pipeline { agent any; stages { stage { steps { echo } } } }开始到后来在多个项目里落地统一的共享库、并行矩阵、按需重跑我愈发觉得 Pipeline 语法的核心并不在于记忆多少指令而在于能否把部署流程拆得足够清晰再让语法有序地服务于流程。如果让我只给一条最核心的建议那就是尽量走声明式把脚本逻辑压到script块内部一旦发现某个流程需要复制到第三个仓库就认真考虑抽共享库。另外调流水线别怕试错Jenkins 的 Replay 功能本身就是最大的试错保险大胆改、快速跑在可见的反馈里迭代比对着文档空想快得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询