简介:拉格朗日方法是机器人动力学建模中的经典方法,尤其适合分析多关节机械臂的动态特性;不过实际编程时往往需要处理矩阵求导、符号运算等问题,有一定门槛。代码基于霍伟《机器人动力学与控制》教材中的公式,在MATLAB环境下采用拉格朗日方法实现机器人动力学方程,并修正了网上流传版本中变量未定义等错误,使代码能直接运行,适合机器人学入门者、控制方向学生以及需要快速搭建动力学模型做算法验证的工程师。rar压缩包内共2个文件,均为.m脚本,一个实现核心动力学计算,一个提供可直接运行的测试示例,整体仅2KB,结构紧凑,便于逐行阅读和二次修改,也可在课程作业、毕业设计或科研仿真中复用。资源已有7826人学习使用,正确性与实用性获得较多用户认可。通过运行测试示例,读者可以直观看到机械臂动力学方程的计算流程,理解惯性矩阵、科氏力和重力项在拉格朗日框架下的具体构造,并可在此基础上调整参数、扩展到其他机器人结构,为后续控制算法设计提供基础。
1. 拉格朗日方法落地第一步:这份MATLAB资源到底能帮你省多少事
做机械臂控制的人都有同一个体验:真正难的不是控制器算法本身,而是先拿到一份可靠、完整的动力学方程。机器人动力学的拉格朗日方法在课本上只有几页公式,但真要自己推一遍,二连杆的 M、C、G 矩阵就能让人推一下午,符号一多就乱,漏一项错一项还很难查。这份资源把拉格朗日方法建模全过程做成了可直接运行的 MATLAB 脚本,对应某经典教材《机器人动力学与控制》的章节体系,从广义坐标定义、动能势能构造、拉格朗日求导展开,到最终整理成 M(q)q̈ + C(q,q̇)q̇ + G(q) = τ 的标准形式,一条线走完。适合正在学机器人学的学生拿来对照推导、做机械臂控制的工程师快速验证算法,以及想把手工推导换成符号计算的科研人员。有了这套脚本,推导过程从“玄学”变成可复现、可改参数、可加控制器的工程内容。
2. 拉格朗日方程先立住:广义坐标、动能势能与广义力的物理对齐
2.1 为什么机器人动力学首选拉格朗日而不是牛顿-欧拉
牛顿-欧拉法从每个连杆的牛顿方程和欧拉方程出发,需要把每个连杆的约束反力都算出来,再在关节处逐个消元。二连杆还能忍,自由度一多,代数消元的书写量和出错率同步上升。拉格朗日法走的是另一条路:它不关心连杆之间的约束力,只用广义坐标下的动能、势能和外力做功来构造标量方程。对机器人控制来说,这个优势直接落在结果上——拉格朗日法整理出来的方程天然就是 M(q)q̈ + C(q,q̇)q̇ + G(q) = τ 这种矩阵结构,而 M、C、G 这三个矩阵恰好是后续做计算力矩控制、自适应控制、阻抗控制时最常用的接口。
代价也明显:符号计算量很大。二连杆的 M、C、G 用人力推还能应付,六轴机械臂的符号表达式规模呈指数增长,手工推导基本不现实。所以这套资源把符号计算交给 MATLAB 的 Symbolic Math Toolbox 来做,人工只负责把运动学关系和能量表达式写对,剩下的求导、展开、化简由机器完成。这也是我认为拉格朗日方法在工程上最值得投入的姿势——理论用拉格朗日立框架,实现用 MATLAB 符号工具箱省时间。
2.2 L = T - V 只是入口:真正决定方程形态的是广义坐标选取
拉格朗日量 L = T - V 写起来只有一行,但广义坐标选得对不对,直接决定后续方程的复杂度。以平面二连杆为例,广义坐标选关节角 q1、q2,广义力就是两个关节的驱动力矩。因为伺服电机装在关节上,力和坐标天然配对,方程推出来直接就是控制接口。如果反着选末端笛卡尔坐标,虽然描述的是同一条机械臂,但广义力必须通过雅可比矩阵转换成关节力矩,凭空多出一层变换。
另一个容易忽视的点是动能 T 的构造。连杆的动能包括平动和转动两部分,平动用质心速度,转动用转动惯量和角速度。代码里最常见的坑就是只写了平动动能,漏掉 0.5I(dq1+dq2)² 这一项,导致惯量矩阵少一块。势能 V 则只看质心高度,写的是 mgy,这里 y 的方向必须和重力方向一致,否则重力项符号会整体出错。
所以看这套资源时我建议先别急着跑代码,先看它怎么定义广义坐标、怎么把每个连杆的质心位置写成 q 的函数,再动后面的部分。坐标定义是整棵树的地基,地基歪了,上面 M、C、G 全跟着歪。
2.3 参数表是代码的“地基”:质量、惯量、质心位置的组织方式
拿到脚本后,第一件事不是跑通,而是把参数表找出来对齐。二连杆测试最常用的参数我整理如下,这套数值量级适合做仿真验证,量纲也统一:
| 物理量 | 符号 | 单位 | 典型数值(二连杆测试用) |
|---|---|---|---|
| 连杆1质量 | m1 | kg | 4.2 |
| 连杆2质量 | m2 | kg | 2.1 |
| 连杆1转动惯量 | I1 | kg·m² | 0.12 |
| 连杆2转动惯量 | I2 | kg·m² | 0.05 |
| 连杆1长度 | l1 | m | 0.42 |
| 连杆2长度 | l2 | m | 0.36 |
| 连杆1质心到关节1 | lc1 | m | 0.21 |
| 连杆2质心到关节2 | lc2 | m | 0.18 |
| 重力加速度 | g | m/s² | 9.81 |
参数组织上我习惯用一个 params 结构体统一管理,字段名和符号变量名严格一一对应。这样后续调用 matlabFunction 生成的动力学函数时,参数顺序不会错位。惯量量级是一个很容易翻车的细节:二连杆的转动惯量通常就在 0.01 到 0.2 kg·m² 这个范围,如果抄到 0.0012,仿真出来的角加速度会大到离谱,ode45 直接跑崩。
3. 用MATLAB把方程推出来:符号计算脚本的拆解与关键参数说明
3.1 用syms定义广义坐标与连杆参数的基本姿势
这套资源里最核心的脚本一定是符号推导部分。第一步是把广义坐标、速度和加速度分别定义为符号变量,同时把连杆质量、惯量、长度等物理量用 syms 声明为实数。下面这段是平面二连杆的标准开头:
% 二连杆平面机械臂广义坐标:q1 肩关节,q2 肘关节,单位 rad syms q1 q2 dq1 dq2 ddq1 ddq2 % 位置、速度、加速度 syms m1 m2 I1 I2 l1 l2 lc1 lc2 g real把 dq1、dq2 作为独立的符号变量而不是用 syms q1(t) 这种函数式定义,是为了后续提取 M、C、G 矩阵时更方便。代价是 d/dt 这一步需要用链式法则手动补上,这个在第 3.3 节会展开。末尾加 real 限定符很重要,它能告诉符号引擎所有参数在实数域,simplify 时不用考虑复数分支,化简速度快很多。
3.2 动能与势能的构造:从质心速度到惯量项
定义好符号变量后,下一步是写出每个连杆质心在惯性系下的坐标,然后对时间求导得到速度。这里的求导可以手写,因为只涉及 cos 和 sin 的链式法则,写成显式表达式反而更直观:
% 质心坐标:以基座为原点,关节角从水平轴正向量起 xc1 = lc1*cos(q1); yc1 = lc1*sin(q1); xc2 = l1*cos(q1) + lc2*cos(q1+q2); yc2 = l1*sin(q1) + lc2*sin(q1+q2); % 质心速度(链式法则展开) vx1 = -lc1*sin(q1)*dq1; vy1 = lc1*cos(q1)*dq1; vx2 = -l1*sin(q1)*dq1 - lc2*sin(q1+q2)*(dq1+dq2); vy2 = l1*cos(q1)*dq1 + lc2*cos(q1+q2)*(dq1+dq2); % 动能 = 平动 + 转动;势能 = 重力势能 T = 0.5*m1*(vx1^2 + vy1^2) + 0.5*I1*dq1^2 + ... 0.5*m2*(vx2^2 + vy2^2) + 0.5*I2*(dq1+dq2)^2; V = m1*g*yc1 + m2*g*yc2; L = T - V;动能表达式里的 0.5I1dq1^2 是连杆1绕关节1的转动,0.5I2(dq1+dq2)^2 是连杆2绕自身质心的转动,角速度是两关节角速度之和。验证动能构造是否合理有个土办法:令 dq2 = 0,第二根连杆相对第一根不转动,整个系统相当于一个刚性体,此时动能应该退化成只含 dq1 平方项的表达式。如果退不出来,说明哪一项写错了。
3.3 拉格朗日求导的三种展开写法与符号整理
从 L = T - V 到运动方程,标准步骤是对广义坐标和广义速度分别求偏导,再做一次时间导数。代码里的写法直接决定后续能不能干净地提出 M、C、G。我一般按下面这种方式做:
% 对符号表达式做偏导 dL_dq1 = diff(L, q1); dL_dq2 = diff(L, q2); dL_ddq1 = diff(L, dq1); dL_ddq2 = diff(L, dq2); % 对时间求导用链式法则手动展开(关键一步) ddt_dL_ddq1 = diff(dL_ddq1, q1)*dq1 + diff(dL_ddq1, q2)*dq2 + ... diff(dL_ddq1, dq1)*ddq1 + diff(dL_ddq1, dq2)*ddq2; ddt_dL_ddq2 = diff(dL_ddq2, q1)*dq1 + diff(dL_ddq2, q2)*dq2 + ... diff(dL_ddq2, dq1)*ddq1 + diff(dL_ddq2, dq2)*ddq2; % 拉格朗日方程:d/dt(dL/ddq) - dL/dq = tau,先算左边 eq1 = ddt_dL_ddq1 - dL_dq1; eq2 = ddt_dL_ddq2 - dL_dq2;这里 ddt_dL_ddq1 是 ∂L/∂dq1 对时间的全导数,它依赖 q1、q2、dq1、dq2 四个变量的变化,所以链式展开有四条路径。这也是为什么前面要用独立符号变量——这样展开路径一清二楚,不容易漏项。
拿到 eq1、eq2 之后,下一步是从方程里提取 M、C、G。常见做法是调用 equationsToMatrix:
% 把方程组整理成 M*ddq = rhs 的形式(rhs 中不含 ddq) [MM, rhs] = equationsToMatrix([eq1, eq2], [ddq1, ddq2]); % 从 rhs 中分离 C 和 G,注意符号方向 CC = -jacobian(rhs, [dq1, dq2]); GG = -subs(rhs, [dq1, dq2], [0, 0]); % 验证提取是否正确:三者相加应该回到原方程左边 diff_check = simplify(MM*[ddq1; ddq2] + CC*[dq1; dq2] + GG - [eq1; eq2]);这里的负号容易写反。 equationsToMatrix 给出的 rhs 实际等于 -C*dq - G,所以 C 是 rhs 关于 dq 的雅可比的负值,G 是令 dq=0 后 rhs 的负值。写完这一行建议用 diff_check 验证一次,如果简化结果不是 0,说明符号方向有问题,回头检查链式展开那一步。
3.4 从符号表达式到可调用函数:matlabFunction的参数顺序问题
符号推导做完,最终要导出一个可被 ode45 和控制器反复调用的函数。matlabFunction 是把符号表达式转成普通 m 函数的标准工具:
matlabFunction(MM, CC, GG, ... 'Vars', {q1 q2 dq1 dq2, m1 m2 I1 I2 l1 l2 lc1 lc2 g}, ... 'File', 'planar2d_dyn.m');Vars 用元胞数组分了两组,第一组是状态变量 q1、q2、dq1、dq2,第二组是物理参数。生成出来的函数签名是 planar2d_dyn(q1, q2, dq1, dq2, m1, m2, I1, I2, l1, l2, lc1, lc2, g),返回三个输出 MM、CC、GG。Vars 的分组顺序决定调用时的传参顺序,这里一旦写错,后面所有调用全部错位。生成之后第一件事是用随机数对比符号表达式和生成函数的数值结果,误差在 1e-10 以下才算合格。这个对比脚本建议保留下来,每次重新导出函数都跑一遍。
4. 把方程跑起来:MATLAB正向动力学仿真与力矩验证流程
4.1 状态方程怎么写:从 M(q)q̈ + C(q,q̇)q̇ + G(q) = τ 到 ode45
有了 planar2d_dyn.m,动力学方程就变成了一个可调用的函数。正向动力学的目标是给定力矩 τ,求状态 x = [q1; q2; dq1; dq2] 随时间的演化。把方程变形得到:
ddq = M \ (tau - C*dq - G);这里用反斜杠而不是 inv(M) 求逆。M 矩阵在特殊位形下可能接近奇异,inv(M) 会把数值误差放大,反斜杠算子更稳。状态方程函数这样封装:
function dx = planar2d_ode(t, x, p) % 状态解包 q = x(1:2); dq = x(3:4); % 参数解包,字段名与符号变量名保持一致 m1 = p.m1; m2 = p.m2; I1 = p.I1; I2 = p.I2; l1 = p.l1; l2 = p.l2; lc1 = p.lc1; lc2 = p.lc2; g = p.g; % 调用符号推导生成的动力学函数 [M, C, G] = planar2d_dyn(q(1), q(2), dq(1), dq(2), ... m1, m2, I1, I2, l1, l2, lc1, lc2, g); tau = zeros(2, 1); % 自由摆动,先不加力矩 ddq = M \ (tau - C*dq - G); dx = [dq(1); dq(2); ddq(1); ddq(2)]; end注意 planar2d_dyn 的调用顺序必须和 matlabFunction 的 Vars 定义完全一致,状态在前,参数在后。这个函数签名是整套资源的公共接口,后续所有仿真、控制器都要经过它。
4.2 给定力矩看运动:二连杆自由摆动的完整流程
封装好 ODE 函数后,主脚本里做四件事:定义参数、设置初值、调 ode45、画结果。二连杆自由摆动是验证动力学方程最朴素也最有效的场景:
p.m1 = 4.2; p.m2 = 2.1; p.I1 = 0.12; p.I2 = 0.05; p.l1 = 0.42; p.l2 = 0.36; p.lc1 = 0.21; p.lc2 = 0.18; p.g = 9.81; % 初值:小角度释放,模拟“松手”过程 x0 = [0.3; 0.2; 0; 0]; tspan = [0 5]; opts = odeset('RelTol', 1e-6, 'AbsTol', 1e-8); [t, x] = ode45(@(t, x) planar2d_ode(t, x, p), tspan, x0, opts); % 绘制关节角度响应 plot(t, x(:,1), '-', t, x(:,2), '--');RelTol 和 AbsTol 的选择是个平衡:默认容差对自由摆动够用,但如果后面要做能量守恒验证,容差必须调小到 1e-6 和 1e-8,否则能量曲线会有肉眼可见的漂移。初值不要给太大角度,二连杆在小角度摆动时的动力学特性更接近线性,方便先确认趋势,再逐步加大角度。
4.3 仿真结果怎么判读:能量守恒和周期性的快速检查
自由摆动没有外力矩,系统总能量应该守恒。检查能量比盯角度曲线更能暴露方程错误:
E = zeros(length(t), 1); for k = 1:length(t) q = x(k, 1:2)'; dq = x(k, 3:4)'; [Mk, ~, ~] = planar2d_dyn(q(1), q(2), dq(1), dq(2), ... p.m1, p.m2, p.I1, p.I2, ... p.l1, p.l2, p.lc1, p.lc2, p.g); % 势能直接代入质心坐标表达式 Vk = p.m1*p.g*p.lc1*sin(q(1)) + ... p.m2*p.g*(p.l1*sin(q(1)) + p.lc2*sin(q(1)+q(2))); E(k) = 0.5 * dq' * Mk * dq + Vk; end % 能量漂移检查:5 秒内相对变化应小于 1% drift = max(E) - min(E); if drift / E(1) < 0.01 disp('能量守恒通过'); else disp('能量异常,检查方程或减小容差'); end如果能量递增,优先怀疑 matlabFunction 导出的 C、G 有符号问题;如果能量随机跳变,多半是 M 矩阵奇异或容差太小。仿真工具的选择上,纯脚本的好处是参数化方便、可复现,Simulink 的优势是能直观搭控制框图,我一般先用脚本打通动力学,再考虑是否上 Simulink。
5. 避坑与排查:拉格朗日方法仿真中五个高频翻车现场
5.1 符号推导没问题,数值仿真却很快发散
现象:ode45 跑不了几步,状态变成 NaN,或者角度呈指数级增长,物理上完全不合理。
原因:最常见的是参数量级错误。惯量写小了、质量写大了,都会让加速度大得离谱;其次是 M 矩阵在某个位形下接近奇异,求解器被迫把步长压到极小,最终数值崩溃。
解决:先做静态合理性测试。令 dq=0,只算 G 矩阵,检查 q=[0;0](水平展开)和 q=[pi/2;0](竖直)两个位置的重力矩是否合理;再任取一个状态,用 rank(M) 和 cond(M) 检查 M 条件数。cond(M) 超过 1e4 说明几何参数有问题,优先查杆长和质心位置。
5.2 重力项方向不对,机械臂像在太空漂浮
现象:q=0 时理论上重力矩应该为零或一个明确的已知值,但仿真里机械臂慢慢漂走,好像重力不存在。
原因:势能 V = mgy 的 y 方向与重力方向没对齐。有人习惯把向下设为正,但质心坐标又按向上为正写了,两者差一个负号,G 矩阵整体反号。
解决:统一约定基准系,y 轴向上,重力势能取 +mgy。改完后用两个静态位置验证:q=[0;0] 时手动算一次重力项,q=[pi/2;0] 时再算一次,确认符号和大小都对再跑仿真。这一步五分钟能完成,能省掉后面一整天的排查。
5.3 C矩阵怎么取都别扭,控制补偿总是不对
现象:控制器里补偿 C*dq 之后系统反而震荡,或者做能量守恒检查时系统总能量有规律地上下浮动。
原因:从拉格朗日方程直接展开出的 C 矩阵并不唯一。只有用 Christoffel 记号构造的 C 才满足 Ṁ - 2C 为斜对称矩阵这个性质,直接从方程里按系数硬拼很容易拼出错误的组合。这个问题在书上只是一个小注记,实际落地时非常坑。
解决:不手拆,直接用 Christoffel 记号重建 C 矩阵:
% 用 Christoffel 记号重建 C,避免从方程里硬拆出错 C_sym = sym(zeros(2, 2)); for i = 1:2 for j = 1:2 for k = 1:2 C_sym(i, j) = C_sym(i, j) + 0.5 * ... (diff(MM(i, j), q1) + diff(MM(i, k), q1)) * dq(k); end end end然后用随机数值检查 Ṁ - 2C 的斜对称性:
h = 1e-7; [Mp, ~, ~] = planar2d_dyn(q(1)+h, q(2), dq(1), dq(2), p.m1, p.m2, p.I1, p.I2, p.l1, p.l2, p.lc1, p.lc2, p.g); [Mm, ~, ~] = planar2d_dyn(q(1)-h, q(2), dq(1), dq(2), p.m1, p.m2, p.I1, p.I2, p.l1, p.l2, p.lc1, p.lc2, p.g); Mdot = ((Mp - Mm) / (2*h)) * dq(1); [Mp, ~, ~] = planar2d_dyn(q(1), q(2)+h, dq(1), dq(2), p.m1, p.m2, p.I1, p.I2, p.l1, p.l2, p.lc1, p.lc2, p.g); [Mm, ~, ~] = planar2d_dyn(q(1), q(2)-h, dq(1), dq(2), p.m1, p.m2, p.I1, p.I2, p.l1, p.l2, p.lc1, p.lc2, p.g); Mdot = Mdot + ((Mp - Mm) / (2*h)) * dq(2); [Mnum, Cnum, ~] = planar2d_dyn(q(1), q(2), dq(1), dq(2), p.m1, p.m2, p.I1, p.I2, p.l1, p.l2, p.lc1, p.lc2, p.g); S = Mdot - 2*Cnum; if norm(S + S', 'fro') < 1e-5 disp('斜对称性质满足'); else disp('C 矩阵有问题,需要重建'); end这个检查值得写进脚本里,每次改动力学参数或重新导出 matlabFunction 后跑一遍。
5.4 matlabFunction生成函数与符号表达式结果不一致
现象:符号表达式里 M11 手算是 0.83,但调用生成的 planar2d_dyn 函数得到 83 或者 0.083。
原因:Vars 里的变量顺序和调用时的传参顺序错位。最常见的是把 l1 和 m1 写反,或者状态变量里 q1、q2 和 dq1、dq2 的顺序不一致。这类错误编译器不会报警,只会静默给出错误数值。
解决:生成函数后立刻做一次“符号 vs 函数”数值对比。用一组随机数,分别用 subs 求符号表达式和调用生成函数,比较输出的 M、C、G 与符号计算差值的范数:
q_test = [0.3; 0.5]; dq_test = [0.2; -0.4]; % 符号计算 M_sym = subs(MM, {q1, q2, dq1, dq2, m1, m2, I1, I2, l1, l2, lc1, lc2, g}, ... {q_test(1), q_test(2), dq_test(1), dq_test(2), p.m1, p.m2, p.I1, p.I2, p.l1, p.l2, p.lc1, p.lc2, p.g}); [M_fun, ~, ~] = planar2d_dyn(q_test(1), q_test(2), dq_test(1), dq_test(2), ... p.m1, p.m2, p.I1, p.I2, p.l1, p.l2, p.lc1, p.lc2, p.g); if norm(double(M_sym) - M_fun, 'fro') < 1e-10 disp('函数导出正确'); else disp('参数顺序有误,检查 Vars'); end这段测试脚本建议跟随资源一直保存,每次导出函数后先过它再进主流程。
5.5 ode45跑得极慢或直接报错
现象:仿真 5 秒花了半分钟,或者报错提示输出参数未赋值、求解器提前终止。
原因:M 矩阵接近奇异时,ode45 为了保证数值稳定会不断压缩步长,导致仿真时间暴涨;另一个常见原因是 matlabFunction 导出前没做简化,生成的文件里塞了大量重复的符号子表达式,每次调用都算一堆冗余操作。
解决:导出前对 MM、CC、GG 分别做一次 simplify,然后再传 matlabFunction。如果 cond(M) 依旧很大,考虑换单位制——杆长用米、质量用公斤、惯量用 kg·m²,别混用厘米和克。这类问题排查时优先看 cond(M) 和能量曲线,不要盲目调求解器参数。
6. 验证与扩展:把二连杆动力学变成控制验证的“标尺”
6.1 用计算力矩法反推动力学方程是否正确
动力学方程写没写对,有一种比能量检查更强的验证方式:用计算力矩法(computed torque)直接做轨迹跟踪。计算力矩法的控制律是:
τ = M(q) * (q̈_des + Kd * ė + Kp * e) + C(q,q̇)q̇ + G(q)
如果 M、C、G 三者来自同一套拉格朗日推导且符号正确,闭环系统会退化成线性二阶系统,误差指数收敛。如果方程有错,哪怕只错一个符号,误差也会发散。下面是一个最小验证脚本:
% 期望轨迹:正弦信号 qd = @(t) [0.3*sin(t); 0.2*cos(t)]; dqd = @(t) [0.3*cos(t); -0.2*sin(t)]; ddqd = @(t) [-0.3*sin(t); -0.2*cos(t)]; Kp = diag([4, 4]); Kd = diag([2, 2]); [t, x] = ode45(@(t, x) closed_loop_ode(t, x, qd, dqd, ddqd, Kp, Kd, p), ... [0 10], [0.2; 0.1; 0; 0], opts); % 计算跟踪误差 e_end = qd(10) - x(end, 1:2)'; fprintf('末端跟踪误差: %g, %g\n', e_end);控制增益从 Kp=4、Kd=2 起步,Kp 先加大,Kd 再跟上,两个增益别同时猛调。如果误差曲线指数收敛,说明动力学方程本身可信,可以拿这套 M、C、G 继续做更复杂的控制器;如果收敛趋势不对,先回归第 5 章的几个检查点。
6.2 调参路径与回归测试习惯
调参路径上我习惯分三步:先让期望轨迹为常值,看稳态误差;再给低频正弦信号,观察跟踪延迟和超调;最后逐步提高频率,找到控制器的带宽边界。每一步只改一个参数,记录误差曲线,别贪快。
我还保留了一个“回归测试”的习惯:固定一组 golden 参数和一条固定轨迹,每次改动符号推导或参数组织方式后,强制跑一遍同一轨迹,对比力矩曲线或误差积分。任何非预期的差分都说明某个环节悄悄变了。这套资源里如果脚本组织得好,回归测试就是改一行参数、跑一个文件的事。从那以后,我每次改完动力学模型都强制走一遍这个流程,再去碰控制器代码。这个习惯帮我挡掉了很多事后才发现的低级错误,希望帮到你。
本文还有配套的精品资源,点击获取