☰
UUV分布式编队一致性控制仿真:应对通信时延、丢包与拓扑干扰
2026/10/28 8:44:37 网站建设 项目流程

简介:一份聚焦无人水下航行器(UUV)六自由度建模与分布式编队保持的仿真工程,面向水下无人系统、海洋工程与智能控制领域的学生和研究者。项目内含单积分器与独轮车模型的编队控制器、虚拟领航者模块,并配有可直接运行的测试脚本和说明文档,可快速复现编队保持算法,观察UUV在三维空间六自由度运动下的控制效果。压缩包共8个文件,以m源码为主,辅以1个Markdown说明文档,整体仅8KB,结构精简,便于按模块深入研读。目前已有285人学习下载,是一份轻量而实用的分布式编队控制参考案例。通过该项目可掌握六自由度动力学建模思路、分布式控制器设计方法及仿真调试流程,对理解李雅普诺夫稳定性、滑模控制等理论在水下编队场景中的应用也很有帮助。

1. DistributedFormationKeeping_master 在跑什么:UUV 编队仿真不是摆好队形,而是对抗丢包与扰动

水下集群编队有个让人印象深刻的瞬间:五条 UUV 以三角形队形前进,突然中间两条的通信链路中断。集中式架构里,整队会停下来等待指挥艇重连;而 DistributedFormationKeeping 这类基于一致性算法的分布式架构,会先用本地邻居信息把队形压住,等链路恢复后再慢慢收敛回理想队形。这个“失联但不乱”的能力,正是这个项目标题背后的核心价值:把分布式编队保持算法部署在 UUV 集群仿真环境里,在非理想通信条件下验证队形控制、拓扑鲁棒性和模型误差。适合正在做水下集群控制、无人艇编队的从业者和研究生。有一点要提前说清楚:这个项目最容易跑通的是理想环境下的队形,真正值钱的仿真工作全在时延、丢包和拓扑切换里。

2. 分布式编队控制选型:一致性算法为什么比领航-跟随更适合 UUV 集群

2.1 三种编队架构的鲁棒性对比:中心化、领航跟随与分布式一致性的边界

做 UUV 编队仿真,第一个躲不开的问题是控制架构选型。常见做法是三种里面挑一个:集中式、领航-跟随、分布式一致性。这三种在陆上机器人和无人机里都有成熟案例,但搬到水下,结论会明显偏向分布式。

集中式要求指挥艇实时拿到全局状态,再算好指令分发给所有成员。它的优势是队形精度高、能全局优化,但代价是通信拓扑必须保证“指挥艇到每一艘艇”双向连通。水下通信带宽低、误码高,这个条件本身就很难满足。更致命的是单点故障:指挥艇断电或链路被干扰,整队变成无头编队。我见过有人把集中式算法封装得很漂亮,第一次仿真跑 100 秒没问题,把指挥艇通信断开十秒,整个编队直接“放羊”。

领航-跟随的优点是通信压力小,跟随者只需要跟踪领航者的状态,不需要和所有邻居交换信息。但它把可靠性押在了领航者身上。UUV 集群里领航艇一旦故障,备选领航者的切换逻辑又得单独写一套。而且跟随者之间不交换状态,队形的横向误差没有闭环约束,稍微一点水流扰动,队形就会从矩形慢慢扭曲成一条斜线。

分布式一致性走的是另一条路:每个成员只和可达邻居交换状态,控制指令由邻居之间的误差项累加生成。没有严格意义上的指挥节点,通信拓扑只要保持连通,队形就能维持。链路丢失时,剩余链路还能继续压住局部误差。代价也很明显:状态交换频率高,整队收敛速度受制于拓扑结构,参数调起来比前两种繁琐。针对 UUV 集群这种“节点不多但通信极不可靠”的场景,我通常首选的还是分布式一致性,DistributedFormationKeeping 这个项目也正好落在这一派。

架构通信依赖单点故障适用场景
集中式全局连通,频率高指挥艇失效即瘫痪小规模、通信可靠、需要全局优化
领航-跟随只需跟随领航者领航者失效即失序规模小、队形相对固定
分布式一致性局部邻居连通即可无严格单点通信弱、节点可靠性差的 UUV 集群

2.2 一阶与二阶一致性:拉普拉斯特征值决定收敛速度,不是调大 K 就行

一致性算法在水下编队里的落点,通常是运动学层面的任务层控制。把每艘 UUV 看成一个质点,状态是水平面位置坐标,控制目标就是让所有成员的位置减去各自编队偏置之后达到一致。一阶一致性的写法很简洁:每个成员的速度指令等于邻居位置误差的加权和。

这里有一个新手最容易搞混的点:编队偏置必须放进误差项里面。如果只让位置一致,所有 UUV 最终会堆在同一个坐标点,那个不是编队,是零队形。正确形式是把每艘艇的目标位置拆成“编队公共中心”加“相对偏置”,控制目标是让 x_i 减 r_i 这个差值在全队收敛到同一个值。收敛速度由通信拓扑的拉普拉斯矩阵第二小特征值 lambda_2 决定,lambda_2 越大,收敛越快。盲目调大控制增益 K,会把测量噪声放大,甚至造成振荡。

二阶一致性则会引入速度和加速度项,适合直接输出推力指令的场景。但 UUV 本体是欠驱动系统,水平面内通常只有纵向推力和艏摇力矩,直接做加速度控制会碰到模型不确定性,反而不如一阶一致性在任务层生成速度指令、再由底层推进器分配来得干净。我自己的习惯是任务层用一阶、底层用单艇控制器,把一致性算法和 UUV 动态模型解耦,这样才能在换 UUV 模型时不动算法。

2.3 UUV 欠驱动模型接入:任务层速度指令如何落到推进器指令

UUV 的水平面运动通常简化为三个自由度:北向位置、东向位置、艏摇角。推进器配置常见的是尾部主推加艏艉舵,部分平台带艏侧推。欠驱动体现在横向没有直接推力,横移是靠艏摇和纵向速度配合出来的。仿真里真没必要一上来就上六自由度水动力模型,先把三自由度运动学模型跑通,再逐步加阻力项和海流扰动。

我一般会把一致性算法输出当成任务层速度指令,包括纵向速度 u_d 和艏摇角速度 r_d。然后单艇控制层用两个 PID 分别跟踪这两个量,把设定值换算成推进器转速和舵角。这样分层的好处是,算法换模型时只需要改单艇控制层,一致性部分完全不动。另一个额外收益是仿真步长可以拉开:任务层 0.1 秒更新一次已经足够,单艇控制层用 0.01 秒,整体仿真速度能快不少,还更容易收敛。

3. 重建最小闭环:把 DistributedFormationKeeping 改造成自己的 UUV 仿真工程

3.1 拿到这类 master 仓库先拆目录:四个模块和一个初始化脚本

从标题可以看出这是一个带 master 标记的工程包。拿到这类仓库,我不会急着跑主脚本,先把目录拆开,确认几个关键模块在哪。常见的组织结构是:算法目录放一致性控制器和拉普拉斯矩阵生成脚本;仿真目录放 Simulink 模型或 MATLAB 主脚本;UUV 模型目录放水动力参数和推进器模型;还有一个实验配置目录放初始化参数和通信参数。

我拿到项目后第一件事是找一个初始化参数脚本,通常叫 init_params.m 或者 setup.m 之类。它决定了仿真的顶层变量:UUV 数量、初始位置、编队偏置矩阵、通信邻接矩阵、控制增益 K。这个脚本里的变量名和后面算法函数是对应的,改模型前先看它,能省掉大量查变量的时间。

还有一个不能忽略的动作:确认 master 分支的版本和你本地 MATLAB 版本是否匹配。老工程常见的问题是用了已废弃的函数,比如旧版通信工具箱的 API,升级 MATLAB 后直接报错。这种情况下我不会去硬改整个工程,而是先建一个干净的最小闭环:三艘 UUV、环形拓扑、固定队形,跑通后再逐步加功能。最小闭环跑通比什么都重要。

3.2 一阶一致性加编队偏置:核心函数与拉普拉斯矩阵生成

最小闭环的核心代码其实很短。下面是带编队偏置的一阶一致性函数,它接收任务层位置、拉普拉斯矩阵、编队偏置和控制增益,输出每艘艇的位置导数值,也就是任务层速度指令。

% formation_consensus_1st.m % 一阶一致性 + 恒定编队偏置,UUV 任务层控制器 function dx = formation_consensus_1st(x, Lap, r_off, K) % 输入: % x : N×3 矩阵,N 艘 UUV 的任务层位置,单位米 % Lap : N×N 拉普拉斯矩阵,由邻接矩阵生成 % r_off : N×3 矩阵,每艘艇相对队形中心的偏置,单位米 % K : 控制增益,标量,典型值 0.1~1.0 % 输出: % dx : N×3 矩阵,任务层速度指令,单位 m/s N = size(x, 1); dx = zeros(N, 3); for i = 1:N acc = zeros(1, 3); for j = 1:N if Lap(i, j) ~= 0 % 编队偏置必须进入误差项:(x_i-r_i)-(x_j-r_j) acc = acc + Lap(i, j) * ((x(j, :) - r_off(j, :)) - ... (x(i, :) - r_off(i, :))); end end dx(i, :) = -K * acc; end end

这个函数里的核心在误差项构造上。Lap(i, j) 是拉普拉斯矩阵的第 i 行第 j 列元素,非零表示 i 能收到 j 的状态。误差项先各自减掉编队偏置,再做邻居间差,这样全体收敛后 x_i 减去 r_i 都相等,队形就保持住了。如果漏掉 r_off,数学上就退化成一致性汇聚,所有 UUV 会挤到一个点上,这个问题我在第 5 章会专门展开。

配套的拉普拉斯矩阵生成脚本也要写清楚。它可以从邻接矩阵出发构建未加权拉普拉斯,也可以做 Metropolis 加权,让不同节点的度差异不至于过大影响收敛速度。

% build_laplacian.m % 从邻接矩阵构建拉普拉斯矩阵,并输出特征值用于判稳 function [Lap, eigenvals] = build_laplacian(adj, weight_type) % 输入: % adj : N×N 邻接矩阵,对角线为 0,对称或非对称均可 % weight_type : 'unweighted' 或 'metropolis' % 输出: % Lap : N×N 拉普拉斯矩阵 % eigenvals : 拉普拉斯特征值向量,第 2 个是 lambda_2 N = size(adj, 1); if strcmp(weight_type, 'metropolis') deg = sum(adj, 2); Lap = zeros(N); for i = 1:N for j = 1:N if adj(i, j) % Metropolis 加权:权重与两节点度数相关 Lap(i, j) = -1 / (max(deg(i), deg(j)) + 1); end end end Lap = Lap - diag(sum(Lap, 2)); else Lap = diag(sum(adj, 2)) - adj; end eigenvals = eig(Lap); fprintf('lambda_2 = %.4f\n', eigenvals(2)); end

用这个脚本跑之前,先用一个简单邻接矩阵验证:闭环三节点拓扑的 lambda_2 是正数,说明图连通,一致性可以收敛。如果 lambda_2 接近 0,说明拓扑接近分裂,不管 K 调多大都压不住队形。Metropolis 加权适合节点度差异大的场景,比如一艘艇有四个邻居、另一艘只有一个邻居,不加权容易让收敛速度被低度节点拖住。

3.3 Simulink 挂 s-function 的固定步长与代数环经验

把一致性函数跑通之后,第二步是把它接进 Simulink,让指令驱动一个简单的 UUV 运动学模型。常见做法是用 Level-2 MATLAB S-Function 封装一致性算法,在输出端接积分器得到位置,再反馈回输入。这个结构简单,但有两个坑:代数环和步长。

代数环出在“位置输入到算法、算法输出速度、速度积分又回到位置”这个回路里。如果 Simulink 检测到直接馈通,会在每个步长里反复迭代求解,很容易出现收敛缓慢甚至报错。我一般会在 S-Function 输出端加一个 Unit Delay,或者把算法改成离散状态更新,彻底切断代数环。另一个省事做法是用 MATLAB Function 块配一个零阶保持器,效果一样。

步长选择上,任务层用固定步长 ode4,步长 0.1 秒比较稳。0.1 秒对水声通信和 UUV 任务层控制都算合理,再小就是浪费算力。单艇控制层如果也放进同一个模型,我会把它做成独立子系统,步长设 0.01 秒,两个子系统之间用 Rate Transition 连接。第一次跑通建议先关掉所有通信干扰,三艘艇从任意初始位置出发,观察队形误差是否在 30 秒内收敛到可接受范围。这一步验证的不是算法多先进,而是模型搭建有没有引入低级错误。

4. 通信链路仿真:时延、丢包和拓扑切换怎样让分布式编队散架

4.1 用 MATLAB 模拟水声信道:丢包率与时延注入的最小实现

一致性算法在实际水下的表现,和理想仿真差距最大的地方就在通信链路。水声通信的典型特征是传播时延大、带宽低、丢包率高,链路质量还随距离和水文条件剧烈波动。想在仿真里逼近真实表现,第一步就是给一致性算法注入丢包和时延。

我写过很多次这个逻辑,核心是把“理想邻居状态”替换成“经过信道后的状态”。下面的函数模拟了固定时延和随机丢包对邻居状态的影响。它用持久化缓存保存历史状态,每次更新时根据丢包率决定用真实状态还是空数据,再按时延取出对应时刻的缓存值。

% lossy_delay_channel.m % 给一致性算法注入丢包和固定时延,返回各艇可用的邻居状态 function [x_used, link_ok] = lossy_delay_channel(x_all, t, delay, loss, seed) % 输入: % x_all : N×3 当前所有 UUV 的任务层位置 % t : 仿真时间,秒 % delay : N×1 各链路时延,秒 % loss : 标量丢包率,0~1,例如 0.2 表示 20% 的包丢失 % seed : 随机种子,保证同参数可复现 % 输出: % x_used : N×3 经过信道后的邻居状态,丢包时用上一时刻值 % link_ok: N×1 链路可用标志 persistent buffer_data buffer_time link_state if isempty(buffer_data) buffer_data = zeros(size(x_all, 1), 3, 20); % 固定缓存长度 20 步 buffer_time = zeros(1, 20); link_state = ones(size(x_all, 1), 1); end % 丢包判定:用种子和当前时间生成可复现的随机序列 rng(seed + round(t * 1000)); link_ok = rand(size(x_all, 1), 1) > loss; % 取时延后的历史状态 delay_step = round(delay / 0.1) + 1; % 假设外层步长 0.1s idx = max(1, mod(round(t * 10) - delay_step + 20, 20) + 1); x_used = buffer_data(:, :, idx); % 当前状态写入缓存,固定长度覆盖最老数据 buffer_time = circshift(buffer_time, 1); buffer_time(1) = t; buffer_data = circshift(buffer_data, 1, 3); buffer_data(:, :, 1) = x_all; end

这个函数的要点有三个。第一,缓存必须限制长度,否则长时间仿真的内存会无限增长,这是很多仿真卡死的隐藏原因。第二,丢包处理用的是“保持旧值”策略,比直接把数据置零温和,更接近真实接收端的缓存机制。第三,随机种子必须由外部传入,这样才能保证同一组参数重复跑结果一致,否则不同实验之间的差异都没法归因。

参数上,丢包率从 0 到 0.3 逐步加,观察队形误差的均方根值变化。时延从 0.1 秒加到 2 秒,重点看有没有出现持续振荡。这两个参数一个破坏连通性、一个破坏相位,症状完全不同。丢包率过高时队形会缓慢漂移,时延过大时队形会来回震荡而不是漂移。

参数建议初值失效模式
丢包率0 ~ 0.3漂移、分裂成两群
固定时延0.1 ~ 2 秒极限环型振荡
通信周期0.5 ~ 2 秒收敛速度下降

4.2 GNSS master 与惯性上浮校正:和 STK/MATLAB 联合仿真怎么衔接

UUV 在水下长期工作时,惯性导航误差会越积越大,必须定期上浮到水面接收 GNSS 信号做校正。这个场景在编队仿真里经常被忽略,但它直接影响编队基准:如果各艇的定位基准漂移不一致,一致性算法交换的就是“各自坐标系下的位置”,数值上再漂亮也白搭。

碰到带 GNSS 的工程,我通常的做法是引入一个 GNSS master 概念。母船或水面浮标作为 GNSS master,播发差分改正给附近上浮的 UUV,各艇以 master 为基准校准自己的位置。仿真里不必真的去模拟卫星信号,只需要在特定时刻触发一次位置重置,把惯导累积误差清掉。触发条件可以设为每 300 秒或当编队误差超过阈值执行一次。

进阶一点的做法是和 STK 这类工具联合仿真。STK 负责给出水面浮标或中继平台的可见性和几何构型,MATLAB 负责把可见性结果转成仿真里的通信链路通断。比如 UUV 在水下时与 GNSS master 链路断开,上浮后链路恢复,这个动态变化就可以驱动 4.1 节里的 link_ok 标志。联合仿真最麻烦的不是接口,而是时序同步。STK 的仿真时间必须和 MATLAB 的仿真时间对齐,我一般用主时钟设在 MATLAB 侧,STK 按固定步长输出数据,不搞双向实时同步,省掉大量调试时间。

4.3 master 选举失效:为什么“servers can't agree on who is the master”这个报错思维可以帮到编队

做分布式系统的人对一类报错很敏感:多进程同时竞选主节点,结果谁也说服不了谁。并行计算里有名的 “servers can't agree on who is the master” 就是典型。这类问题的本质是选主协议没有唯一收敛点,参与各方各自持有不同的全局视图。

UUV 编队里也存在同构问题。如果编队算法里设计了一个虚拟领航者,但虚拟领航者由各艇自行计算,那么遇到丢包时,不同艇对“队形中心”的估计会分裂。有的艇认为中心在东侧,有的认为在西侧,队形就从中间裂成两半,各自围绕一个中心收敛。这个现象在图像上非常明显:不是振荡,不是漂移,而是编队整体一分为二。

解决办法不是把虚拟领航者改成某艘实艇,而是设计一个无歧义的公共参考。我的习惯是用编号排序:队内编号最小的艇作为临时基准,其他艇通过一致性协议跟踪这个基准的计算结果。如果最小编号艇掉线,自动切换到第二小编号。这个机制很简单,但需要每个节点都对“当前谁是基准”达成一致,所以它本质上也是选主问题。仿真里我会专门做一个测试用例:让基准艇在仿真中间掉线,观察其他艇能否在 10 秒内对新的基准收敛。如果这个测试过不了,整个编队系统就不具备水下服役的基本条件。

5. 常见问题与避坑记录:从仿真发散到 Git 分支迁移

5.1 仿真发散 NaN/Inf:步长、代数环与初始化三连查

现象:仿真跑了几秒,Scope 里位置曲线直接飞向 1e10 量级,或者波形显示 NaN。

原因一般是三种:固定步长太大导致数值积分失稳;S-function 内部代数环反复迭代不收敛;初始状态里有 NaN 或无穷大。步长过大时,积分器一个步长就跨过了模型的稳定边界,误差逐级放大。

解决:先把求解器换成固定步长 ode4,步长从 0.01 秒开始试。如果模型里有 S-function,查一遍是否存在输出直接依赖输入的通路,有就在输出侧加 Unit Delay。初始化脚本里用打印函数检查每个状态量,确保没有 NaN。我自己的经验是 80% 的发散问题出在初始化,而不是算法本身。

5.2 队形收敛到原点:编队偏置放错位置的经典错误

现象:三艘 UUV 从不同初始位置出发,最终全部汇合到坐标系原点,队形完全消失。

原因是把一致性项写成了 x_i 减 x_j,漏掉了偏置项 r_off。这个错误我在不少仓库里见过,因为标准一致性算法的形式太简洁,加偏置这种工程细节反而容易被忽略。

解决:检查核心公式,确保误差项是 (x_i - r_off_i) 减去 (x_j - r_off_j),而不是直接位置相减。另外检查偏置矩阵 r_off 是不是被后续脚本覆盖成零矩阵。有个比较简单的工作方式:初始化后打印一次 r_off,确认每艘艇的偏置各不相同。

5.3 模型输出“红线”:状态量未初始化与 yaw 奇异

现象:Simulink Scope 里波形是红色断线,看不到正常数据。这个现象做数字仿真的人应该很熟,跟 FPGA 仿真里 modelsim 波形是红线几乎是同一个道理:信号没有初始化。

原因:UUV 状态向量初始值全零,艏摇角为 0 度时旋转矩阵在某些表示下会奇异,或者 S-function 里的状态变量从未被写入初值。

解决:初始化脚本里给位置和艏摇角加一个很小的扰动,比如 1e-3 量级,绕过奇异点。不要用严格零作为初值,这在数值上极其脆弱。排查时先用 Probe 信号直接看状态向量的第一维,确认是否有 NaN。

5.4 仿真卡死只能任务管理器强杀:S-function 循环与缓存无限增长

现象:仿真跑上十几分钟,MATLAB 界面无响应,鼠标转圈,最后只能从任务管理器强杀进程。

原因:最常见的是 S-function 里写了 while 循环等待某个外部事件,事件没发生就死循环;另一种是通信缓存无限增长,内存被耗尽。

解决:S-function 里不建议用 while(true) 做事件等待,改用触发子系统或者条件执行子系统。缓存数组预先分配并限制长度,满了就覆盖最老数据,不要动态追加。仿真数据记录也要设上限,只保留最后几万步。强杀进程前先确认模型文件已经保存过,MATLAB 崩溃丢脚本的教训我这里太多了。

5.5 在 master 上写完了代码想迁到 dev:cherry-pick 比 stash 更稳

现象:直接在 master 分支上改代码,改到一半发现应该在 dev 分支开发,又不想把改动丢掉。这是版本管理里最经典的后悔药场景,跟“git 上我在 master 上写的代码怎样剪切到 dev 上”这类问题完全一致。

原因:master 通常被其他协作者或发布流程占用,不太适合直接长出一堆实验性改动。

解决:最稳妥的是基于当前 master 创建新分支,不要先切分支再找改动。执行流程如下:

# 基于当前 master 状态创建 dev 分支 git checkout -b dev master # 提交当前所有改动到 dev git add -A git commit -m "wip: formation consensus with lossy channel" # 把 master 重置回改动前的位置,假设只多了一个提交 git checkout master git reset --hard HEAD~1 # 如果已经 push 到远端,需要强推,但要先和团队确认 git push origin dev

先用 git status 确认工作区状态,再用 git diff 检查改动内容,避免把无关文件一起带走。万一已经 commit 了多个提交,cherry-pick 具体提交比 reset 更合适。这里的关键习惯是:永远不要直接在主分支上做实验性开发,新建分支的成本几乎为零。

6. 没有实艇怎么验收:量化三指标与迁移到 Gazebo 的时序技巧

6.1 队形误差、保持率、通信开销三指标

仿真跑通只是开始,真正要回答的是“这套系统能不能验收”。没有实艇时,我习惯用三个可量化指标给编队算法打分。第一个是队形误差,计算方式是所有艇相对理想队形位置的偏差,取最大偏差或 RMS 值。第二个是编队保持率,统计仿真时间内误差小于阈值的时长占比,阈值一般取艇长的 0.2 倍左右。第三个是通信开销,统计单位时间内交换的消息数量与包大小,这个指标直接对应真实水声通信的能耗。

指标计算方式验收参考
队形误差 RMS每时刻误差取 RMS,再对时间平均小于 0.2 倍艇长
编队保持率误差小于阈值的时间占比95% 以上
通信开销消息数量 × 包字节数 / 仿真时长只做相对比较

6.2 用 lambda_2 反推收敛时间常数:调试时先调拓扑再调增益

一阶一致性的收敛时间常数可以近似表示为 1 除以 K 乘 lambda_2。也就是说,收敛太慢时先不要急着把 K 翻倍,而是检查拓扑的 lambda_2 是不是太小。链式拓扑的 lambda_2 远小于全连通拓扑,盲目调 K 会让系统变得敏感。我的调试习惯是固定 K,先换拓扑,观察 lambda_2 变化,再微调 K。这么做的好处是参数调整有物理意义,不会陷入“K 调大了振荡、调小了太慢”的循环里。

6.3 迁到 ROS/Gazebo 的仿真节点时序:固定步长随机种子的统一

从 MATLAB 迁到 Gazebo 仿真环境时,最大的坑是时序模型不一致。MATLAB 的固定步长在 ROS 里要用定时器节点替代,不能写 while 循环做自己的时钟。丢包和信道噪声的随机种子也必须从启动参数传入,否则每次 Gazebo 启动结果都不一致,根本没法对比算法改进。Gazebo 的水下环境模型成熟度不如地面机器人,但作为一致性算法的验证载体已经足够,重点验证的是通信节点之间的数据流是否正确,而不是水动力精度。我自己吃过亏的教训是:Gazebo 里通信用 ROS topic 模拟,topic 的队列长度必须显式设置,默认值在丢包率高的时候会悄悄丢消息,看起来像是算法问题,其实是消息队列配置问题。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询