资讯动态

离线安装Docker:面向等保与信创的生产级脚本实践

发布时间:2026/10/28 3:50:34 来源:尧图企业网站定制
1. 为什么离线装Docker不是“备选方案”而是生产环境的刚性需求你可能已经习惯了在开发机上敲一句sudo apt install docker.io或curl -fsSL https://get.docker.com | sh三秒搞定。但真正进过交付现场、参与过金融系统迁移、接手过某地政务云二期建设的人都会在第一次看到客户那台连内网都只开HTTP白名单、物理隔离、U盘需登记消杀的服务器时默默把终端关掉——因为那句熟悉的命令连DNS解析都会卡在get.docker.com的域名查询上。这不是理论推演是我在某次为某高校实验室部署AI训练平台时的真实经历。整套系统要部署在无外网的高性能计算集群管理节点上所有软件包必须经校方安全审计后统一导入。当时我带了三套方案源码编译、二进制包手动解压、脚本化离线安装。前两套在测试环境跑通了但到了正式环境运维同事指着审计报告说“你这个二进制包没签名SHA256校验值没进白名单源码编译依赖的go版本和git工具链也不在基线清单里。”最后扛住压力落地的是第三套一个不到120行的Bash脚本配合一个压缩包全程不联网、不调用外部仓库、不生成临时网络请求从挂载U盘到docker --version回显7分23秒完成。这就是离线安装Docker的本质它不是“网络不好时的权宜之计”而是安全合规体系下的标准动作。它强制你回答三个问题这个Docker二进制到底从哪来来源可信性它有没有被篡改完整性校验它依赖的内核模块、cgroup配置、systemd服务模板是否与目标系统完全匹配环境适配性关键词里虽然空着但实际场景中“离线”背后绑定的是“等保三级”“信创适配”“国产化替代”“军工涉密网络”“医疗HIS系统隔离区”等一系列硬性约束。而“脚本自动安装”这六个字意味着你要把原本需要人工判断的二十多个决策点——比如该用docker-ce还是docker-ce-cli的独立包containerd要不要单独安装runc版本是否与内核兼容——全部编码固化让脚本自己做选择。我见过太多人把“离线安装”简单理解为“把.deb或.rpm文件拷过去用dpkg -i或rpm -ivh硬装”结果在启动服务时报错Failed to start docker.service: Unit not found或者运行容器时提示failed to create endpoint: failed to add interface: operation not supported。这些都不是Bug是脚本没把“环境感知”这件事做透。真正的离线脚本得像老司机开车知道前面是陡坡要降档知道雨天要提前轻刹知道这台机器的/proc/sys/fs/aio-max-nr设得太低得先调上去再启服务。所以别再把这事当成“复制粘贴几个命令”的体力活。它是一次对Linux系统底层机制的集中检验从包管理器原理、systemd单元文件加载顺序、cgroup v1/v2混合模式识别到内核模块自动加载规则。接下来的内容我会带你从零写出一个能通过等保测评的离线Docker安装脚本——不靠运气不靠试错每一步都有依据每一处报错都能反向定位根因。2. 脚本设计的底层逻辑为什么不能直接打包官方DEB/RPM很多人第一反应是“Docker官网不是提供.deb和.rpm包吗下载下来U盘拷过去apt install ./docker-ce_*.deb不就完了”——这个思路在个人笔记本上大概率能跑通但在生产环境它会踩中三个致命坑。2.1 依赖树黑洞你以为的“单包”其实是“依赖迷宫”以Ubuntu 22.04为例官方docker-ce24.0.7的.deb包声明了如下关键依赖Depends: containerd.io ( 1.6.24), docker-ce-cli ( 24.0.7), iptables ( 1.4.19), libseccomp2 ( 2.3.0), libc6 ( 2.34), systemd ( 249.11)注意看containerd.io和docker-ce-cli是独立包不是嵌套在主包里的子目录。apt install ./xxx.deb会尝试从本地仓库解析依赖但默认配置下apt根本不会扫描当前目录找其他.deb文件——它只会报错The following packages have unmet dependencies: docker-ce : Depends: containerd.io ( 1.6.24) but it is not installable Depends: docker-ce-cli ( 24.0.7) but it is not installable你得手动把containerd.io_*.deb、docker-ce-cli_*.deb、docker-buildx-plugin_*.deb、docker-compose-plugin_*.deb全下载齐再按依赖顺序执行dpkg -i——而这个顺序dpkg本身不校验全靠人肉记忆。我曾见某团队漏装docker-ce-rootless-extras导致后续启用rootless模式时dockerd-rootless-setuptool.sh脚本直接报command not found排查两小时才发现是插件包缺失。2.2 内核兼容性断层同一个DEB包在CentOS和Ubuntu上行为不同Docker官方RPM包如docker-ce-24.0.7-1.el7.x86_64.rpm明确标注支持el7但实际在CentOS Stream 8上安装后systemctl start docker会失败日志显示Error: failed to start daemon: Devices cgroup isnt mounted原因在于CentOS Stream 8默认启用cgroup v2而该RPM包内置的/usr/lib/systemd/system/docker.service文件里ExecStart参数仍硬编码--exec-opt native.cgroupdriversystemd没适配cgroup v2的挂载点路径。你得手动编辑service文件把--exec-opt改成--cgroup-managercgroupfs或者补上--cgroup-parent/docker。而这个修改不可能写进RPM的%post脚本里——因为RPM构建时根本不知道目标机器的cgroup版本。更麻烦的是Ubuntu系和RHEL系的systemd单元文件路径、默认配置目录、selinux策略位置全都不一样。一个RPM包想同时适配两者要么放弃精细控制要么把所有分支逻辑塞进%post脚本——而这恰恰违背了RPM“声明式配置”的设计哲学。2.3 安全审计死结二进制包签名无法验证等于裸奔Docker官方提供的.deb/.rpm包虽有GPG签名但验证流程极其脆弱你需要先导入Docker官方GPG公钥curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg然后在/etc/apt/sources.list.d/docker.list里指定[archamd64 signed-by/usr/share/keyrings/docker-archive-keyring.gpg]最后apt update apt install docker-ce问题来了第一步的curl命令在离线环境根本执行不了第二步的signed-by路径在离线机上不存在第三步的apt update会因无法连接download.docker.com而失败。你只能退回到dpkg --ignore-dependsall -i xxx.deb这种危险操作——它绕过了所有依赖检查和签名验证相当于把一个未经审核的二进制直接注入系统。提示等保2.0要求“关键应用软件应使用经国家认证的正版软件并验证其完整性”。dpkg --ignore-depends这种操作审计时会被直接判定为“未落实软件供应链安全管控”。所以真正可靠的离线方案必须满足三个铁律来源可溯所有二进制必须来自Docker官方Release页面的tar.gz归档如https://download.docker.com/linux/static/stable/x86_64/docker-24.0.7.tgz该归档提供SHA256SUMS文件可离线校验依赖自洽脚本需内置containerd、runc、docker-proxy等所有组件不依赖系统包管理器解析环境自适应脚本能自动探测cgroup版本、systemd存在性、iptables/nftables模式并生成对应配置。这正是我们脚本设计的起点放弃对.deb/.rpm的幻想回归Docker最原始的形态——静态二进制配置即代码。3. 核心组件拆解从docker-24.0.7.tgz里挖出哪些文件才够用Docker官方提供的docker-24.0.7.tgz位于https://download.docker.com/linux/static/stable/x86_64/是一个精简的静态二进制包解压后结构如下$ tar -tzf docker-24.0.7.tgz docker/ ├── dockerd # Docker守护进程主程序 ├── docker # Docker CLI客户端 ├── docker-init # init进程包装器用于容器内PID 1 ├── docker-proxy # 网络端口映射代理 ├── runc # OCI运行时containerd调用它 └── containerd # containerd守护进程Docker 20.10默认集成但请注意这个包不包含containerd.io的完整服务单元文件也不包含dockerd所需的默认配置模板。如果你直接把docker/目录拷到/usr/bin/然后执行dockerd会遇到两个经典问题dockerd启动后立即退出日志显示failed to load listeners: no sockets founddocker run hello-world报错Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?根源在于dockerd需要一个明确的监听地址如-H unix:///var/run/docker.sock还需要containerd作为底层运行时协同工作。而官方tar包里的containerd二进制只是一个裸进程没有配套的containerd.service单元文件也不会自动创建/var/run/containerd/目录。因此一个生产级离线脚本必须从这个tar包里提取并重组以下六类核心资产资产类型来源位置用途说明是否必需Docker主二进制docker/dockerd启动Docker守护进程处理API请求、镜像管理、容器生命周期✅ 必需CLI客户端docker/docker提供docker命令用户交互入口✅ 必需containerd二进制docker/containerd作为Docker的底层运行时管理容器生命周期、镜像存储、网络插件✅ 必需runc二进制docker/runcOCI规范实现真正创建和运行容器进程的底层工具✅ 必需默认配置模板官方GitHub仓库moby/moby的contrib/init/systemd/目录包含docker.service、docker.socket、containerd.service等systemd单元文件✅ 必需守护进程配置/etc/docker/daemon.json模板控制Docker行为存储驱动、日志选项、镜像仓库、cgroup驱动等⚠️ 推荐其中systemd单元文件必须从源码仓库获取而非依赖系统自带。因为不同发行版的unit文件差异极大Ubuntu 20.04的docker.service里ExecStart是/usr/bin/dockerd -H fd:// --containerd/run/containerd/containerd.sockCentOS 7的docker.service里ExecStart是/usr/bin/dockerd -H fd:// --containerd/var/run/containerd/containerd.sock而新版Docker 24.x要求--containerd指向/run/containerd/containerd.sock注意是/run而非/var/run。脚本必须根据目标系统自动选择正确的unit文件版本。我的做法是在脚本同目录下预置三个版本的unit文件unit-ubuntu2204/适配Ubuntu 22.04cgroup v2默认unit-centos7/适配CentOS 7cgroup v1默认unit-generic/通用版不硬编码cgroup路径由脚本动态注入脚本启动时执行# 探测发行版和cgroup版本 if grep -q cgroup2 /proc/filesystems; then UNIT_DIRunit-ubuntu2204 elif grep -q cgroup /proc/filesystems; then UNIT_DIRunit-centos7 else UNIT_DIRunit-generic fi这样同一份脚本在不同系统上会加载不同的unit文件避免“一份配置打天下”的粗暴逻辑。注意runc和containerd的版本必须严格匹配。Docker 24.0.7要求runc 1.1.12containerd 1.7.12。如果混用旧版runc如1.0.3docker run会报runc did not terminate successfully。脚本必须校验二进制版本号不匹配则拒绝安装。4. 脚本实战从零编写一个可审计、可复现的离线安装器现在我们把前面所有逻辑落地为一个真实可用的Bash脚本。这个脚本命名为install-docker-offline.sh设计原则是最小依赖、最大透明、零网络调用、全路径可控。4.1 脚本整体结构与执行流程脚本采用模块化设计共分五大阶段每个阶段有明确输入输出和失败退出码阶段执行命令关键动作失败退出码0. 环境预检check_prerequisites检查root权限、tar/curl/systemctl是否存在、磁盘空间是否≥500MB11. 组件解压extract_binaries解压docker-24.0.7.tgz到临时目录校验SHA256提取dockerd/docker/runc等22. 系统适配adapt_to_system探测发行版、cgroup版本、SELinux状态选择对应unit文件生成daemon.json模板33. 文件部署deploy_files将二进制拷到/usr/bin/unit文件拷到/usr/lib/systemd/system/创建必要目录44. 服务启停start_docker_service重载systemd配置启动containerd和docker服务验证docker version输出5整个脚本不依赖任何外部库仅用POSIX Shell语法可在dash、bash、zsh下运行。所有路径使用绝对路径避免$PATH污染导致的误判。4.2 关键函数详解如何让脚本“读懂”你的系统check_prerequisites()—— 不是简单检查命令存在而是验证功能完备性很多脚本只写command -v systemctl /dev/null 21 || { echo systemctl not found; exit 1; }这不够。systemctl存在不代表它能正常工作。我们加一层深度检测check_prerequisites() { # 检查root权限比UID0更可靠 if ! id -u | grep -q ^0$; then echo ERROR: This script must be run as root return 1 fi # 检查tar是否支持--warningno-unknown-keywordGNU tar特有避免解压警告干扰 if tar --help 21 | grep -q warning; then TAR_CMDtar --warningno-unknown-keyword else TAR_CMDtar # BusyBox tar不支持该参数用基础版 fi # 检查systemctl是否真能通信避免chroot环境假阳性 if ! systemctl list-units --typeservice --stateloaded /dev/null 21; then echo ERROR: systemctl is not functional (check if systemd is running) return 1 fi # 检查磁盘空间/usr和/var各需200MB local usr_free$(df /usr | tail -1 | awk {print $4}) local var_free$(df /var | tail -1 | awk {print $4}) if [ $usr_free -lt 209715 ] || [ $var_free -lt 209715 ]; then echo ERROR: Insufficient disk space in /usr or /var ( 200MB) return 1 fi }adapt_to_system()—— 精确识别系统特征避免“一刀切”配置这是脚本最核心的智能模块。它不依赖lsb_release很多最小化系统不装而是直接读取系统文件adapt_to_system() { # 1. 识别发行版优先级/etc/os-release /etc/redhat-release /etc/debian_version if [ -f /etc/os-release ]; then . /etc/os-release DISTRO_ID$ID DISTRO_VERSION$VERSION_ID elif [ -f /etc/redhat-release ]; then DISTRO_IDrhel DISTRO_VERSION$(cat /etc/redhat-release | sed -n s/.*release \([0-9]\\).*/\1/p) elif [ -f /etc/debian_version ]; then DISTRO_IDdebian DISTRO_VERSION$(cat /etc/debian_version | cut -d. -f1) else DISTRO_IDunknown DISTRO_VERSIONunknown fi # 2. 探测cgroup版本关键决定socket路径和驱动选项 if mount | grep -q cgroup2\|cgroup2s; then CGROUP_VERSIONv2 CONTAINERD_SOCKET/run/containerd/containerd.sock DOCKERD_CGROUP_DRIVERsystemd # cgroup v2下推荐systemd驱动 else CGROUP_VERSIONv1 CONTAINERD_SOCKET/var/run/containerd/containerd.sock DOCKERD_CGROUP_DRIVERcgroupfs # cgroup v1下cgroupfs更稳定 fi # 3. 检测SELinux状态影响/var/lib/docker路径权限 if command -v getenforce /dev/null 21 getenforce | grep -q Enforcing; then SELINUX_ENABLEDtrue # SELinux Enforcing下/var/lib/docker需打特定标签 CHCON_CMDchcon -t container_file_t else SELINUX_ENABLEDfalse CHCON_CMD: fi # 4. 选择unit文件目录 case $DISTRO_ID in ubuntu|debian) if [ $CGROUP_VERSION v2 ]; then UNIT_DIRunit-ubuntu2204 else UNIT_DIRunit-debian11 fi ;; rhel|centos|rocky|almalinux) UNIT_DIRunit-centos7 ;; *) UNIT_DIRunit-generic ;; esac }deploy_files()—— 原子化部署失败自动回滚部署不是简单cp而是带校验和回滚的原子操作deploy_files() { # 创建临时工作目录 TMP_DIR$(mktemp -d) trap rm -rf $TMP_DIR EXIT # 解压unit文件到临时目录 $TAR_CMD -xzf $UNIT_DIR.tar.gz -C $TMP_DIR # 1. 部署二进制先备份原文件再覆盖 for bin in dockerd docker runc containerd docker-proxy; do if [ -f /usr/bin/$bin ]; then mv /usr/bin/$bin /usr/bin/$bin.bak.$(date %s) fi cp $EXTRACTED_DIR/docker/$bin /usr/bin/$bin chmod x /usr/bin/$bin done # 2. 部署systemd unit文件先停服务再覆盖 systemctl stop docker containerd 2/dev/null || true cp $TMP_DIR/docker.service /usr/lib/systemd/system/docker.service cp $TMP_DIR/containerd.service /usr/lib/systemd/system/containerd.service cp $TMP_DIR/docker.socket /usr/lib/systemd/system/docker.socket # 3. 创建必要目录并设置权限 mkdir -p /var/lib/docker /var/run/docker.sock /etc/docker chown root:root /var/lib/docker chmod 700 /var/lib/docker # 4. 生成daemon.json动态注入cgroup驱动 cat /etc/docker/daemon.json EOF { storage-driver: overlay2, log-driver: json-file, log-opts: { max-size: 10m, max-file: 3 }, exec-opts: [native.cgroupdriver$DOCKERD_CGROUP_DRIVER], live-restore: true, default-ulimits: { nofile: { Name: nofile, Hard: 65536, Soft: 65536 } } } EOF # 5. SELinux上下文修复如果启用 if [ $SELINUX_ENABLED true ]; then $CHCON_CMD -R /var/lib/docker fi }4.3 启动与验证为什么systemctl start docker之后还要多做三件事很多脚本到systemctl start docker就结束了但生产环境必须做三重验证服务状态验证systemctl is-active docker返回active且systemctl is-failed docker返回inactiveSocket连通性验证curl --unix-socket /var/run/docker.sock http://localhost/version | jq -r .Version能正确输出版本号功能闭环验证docker run --rm hello-world输出Hello from Docker!且退出码为0。我们的start_docker_service()函数这样实现start_docker_service() { # 1. 重载配置 systemctl daemon-reload # 2. 启动containerdDocker的依赖服务 systemctl enable containerd systemctl start containerd if ! systemctl is-active --quiet containerd; then echo ERROR: containerd failed to start return 1 fi # 3. 启动docker systemctl enable docker systemctl start docker if ! systemctl is-active --quiet docker; then echo ERROR: docker daemon failed to start journalctl -u docker -n 50 --no-pager return 1 fi # 4. 验证socket避免docker.sock权限问题 if ! curl --unix-socket /var/run/docker.sock http://localhost/version /dev/null 21; then echo ERROR: docker socket not accessible ls -l /var/run/docker.sock return 1 fi # 5. 运行hello-world终极验证 if ! docker run --rm hello-world /dev/null 21; then echo ERROR: docker run hello-world failed return 1 fi echo SUCCESS: Docker installed and verified echo Run docker --version to confirm }5. 实战避坑指南那些文档里不会写的血泪教训写完脚本只是开始真正考验功力的是在各种“奇葩”环境中让它稳定运行。以下是我在十多个离线项目中踩过的坑以及对应的解决方案。这些经验比脚本本身更有价值。5.1 坑dockerd启动后立即OOM Killeddmesg显示Out of memory: Kill process dockerd现象脚本执行成功systemctl status docker显示active (running)但几秒后变成failedjournalctl -u docker里只有Process dockerd (PID XXXX) has been killed by OOM killer。根因dockerd默认内存限制为0无限制但在某些内核版本如CentOS 7.9的3.10.0-1160内核上cgroup v1的memory.limit_in_bytes文件若为-1会导致OOM Killer误判。而离线环境往往资源紧张dockerd自身占用150MB内存加上containerd的120MB很容易触发。解决方案在daemon.json中强制设置内存限制{ default-runtime: runc, runtimes: { runc: { path: runc } }, oom-score-adjust: -500, // 降低OOM优先级 default-ulimits: { memlock: { Name: memlock, Hard: -1, Soft: -1 } } }并在脚本部署后手动执行# 为dockerd进程组设置OOM score echo -500 /proc/$(pgrep dockerd)/oom_score_adj提示oom-score-adjust范围是-1000最不易被杀到1000最易被杀-500是生产环境经验值既保证dockerd不被轻易杀死又不至于影响其他关键进程。5.2 坑docker run报错failed to create endpoint: failed to add interface: operation not supported现象docker version正常docker info能显示信息但docker run ubuntu:22.04 /bin/bash失败错误指向网络接口创建。根因目标系统内核未编译CONFIG_VETH虚拟以太网设备或CONFIG_BRIDGE网桥支持模块。常见于定制化内核如某些国产OS裁剪版或老旧内核3.10。诊断命令# 检查veth模块是否加载 lsmod | grep veth # 检查内核配置需有/config文件 zcat /proc/config.gz 2/dev/null | grep -E (VETH|BRIDGE) || cat /boot/config-$(uname -r) | grep -E (VETH|BRIDGE)解决方案脚本增加内核模块检查环节check_kernel_modules() { local required_modules(veth bridge nf_nat xt_conntrack) for mod in ${required_modules[]}; do if ! lsmod | grep -q ^$mod ; then echo WARNING: Kernel module $mod not loaded # 尝试modprobe如果模块存在 if modprobe $mod 2/dev/null; then echo INFO: Loaded kernel module $mod else echo ERROR: Kernel module $mod not available. Please check kernel config. return 1 fi fi done }5.3 坑docker build过程中COPY指令超时context deadline exceeded现象构建镜像时COPY . /app卡住最终报context deadline exceeded但docker version和docker run都正常。根因离线环境常禁用IPv6而Docker 23.0默认启用IPv6 DNS解析。当/etc/resolv.conf里只有IPv4 nameserver时dockerd会尝试用IPv6连接DNS超时后才回落IPv4导致整个构建过程延迟。解决方案在daemon.json中显式禁用IPv6{ ipv6: false, fixed-cidr-v6: , dns: [114.114.114.114, 8.8.8.8] }并在脚本中自动检测并修复# 检测系统是否禁用IPv6 if sysctl net.ipv6.conf.all.disable_ipv6 | grep -q 1; then echo INFO: IPv6 disabled system-wide, setting \ipv6\: false in daemon.json # 在daemon.json中插入ipv6:false fi5.4 坑docker login到私有仓库失败提示x509: certificate signed by unknown authority现象离线环境部署了Harbor私有仓库docker login harbor.example.com报证书错误。根因Docker守护进程不读取系统的/etc/ssl/certs/ca-certificates.crt而是使用自己的证书存储。离线环境下/etc/docker/certs.d/目录为空。解决方案脚本提供证书注入接口# 如果用户提供了ca.crt文件则自动部署 if [ -f ./harbor-ca.crt ]; then mkdir -p /etc/docker/certs.d/harbor.example.com:443 cp ./harbor-ca.crt /etc/docker/certs.d/harbor.example.com:443/ca.crt echo INFO: Injected Harbor CA certificate fi6. 扩展与维护如何让这个脚本支撑未来三年的升级需求一个脚本的价值不在于它今天能跑通而在于它能否在未来三年持续可用。为此我设计了三个可持续维护机制6.1 版本热更新不改脚本只换压缩包脚本本身不硬编码Docker版本号。它通过一个配置文件docker-version.conf读取版本# docker-version.conf DOCKER_VERSION24.0.7 DOCKER_SHA256a1b2c3d4e5f6... # 对应docker-24.0.7.tgz的SHA256脚本启动时加载if [ -f docker-version.conf ]; then . docker-version.conf else DOCKER_VERSION24.0.7 DOCKER_SHA256a1b2c3d4e5f6... fi升级时只需下载新版本docker-${NEW_VERSION}.tgz和SHA256SUMS计算新SHA256更新docker-version.conf重新打包docker-${NEW_VERSION}.tgz和unit-*.tar.gz到U盘。脚本逻辑完全不变真正做到“配置与代码分离”。6.2 多架构支持x86_64、ARM64、RISC-V一键切换脚本增加ARCH参数./install-docker-offline.sh --arch arm64内部根据--arch参数选择对应压缩包docker-${VER}-x86_64.tgzdocker-${VER}-aarch64.tgzdocker-${VER}-riscv64.tgz并自动适配runc和containerd的交叉编译版本。对于国产化场景如鲲鹏、飞腾这省去了手动替换二进制的繁琐步骤。6.3 审计日志自动生成每次安装留下不可篡改的证据脚本末尾自动生成审计日志generate_audit_log() { cat /var/log/docker-offline-install-$(date %Y%m%d-%H%M%S).log EOF Docker Offline Installation Audit Log Timestamp: $(date -Iseconds) Hostname: $(hostname) Kernel: $(uname -r) Docker Version: $DOCKER_VERSION Installer Script: $(basename $0) v1.0 SHA256 Checksum: $DOCKER_SHA256 System Info: $(lsb_release -a 2/dev/null || echo No lsb_release) $(cat /proc/version) Installation Result: SUCCESS EOF # 设置日志为只读防止篡改 chmod 400 /var/log/docker-offline-install-$(date %Y%m%d-%H%M%S).log }这份日志可直接提交给等保测评机构证明安装过程全程可控、可追溯、可验证。我在某次金融行业项目交付时客户安全团队要求提供“Docker安装全过程的数字指纹”。这份自动生成的日志配合U盘的MD5校验值成为他们签字放行的关键依据。技术人的专业有时候就藏在这些不起眼的细节里。最后分享一个小技巧把这个脚本和

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑