ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MyDAC v5.10 源码级MySQL数据访问组件深度解析与定制指南

MyDAC v5.10 源码级MySQL数据访问组件深度解析与定制指南

简介:本资源是Delphi与C++Builder开发者专用的MySQL数据库直连组件MyDAC v5.10源码包,面向使用Delphi 7–2007及C++Builder开发MySQL应用的中高级开发者,解决原生数据库连接性能低、依赖中间件、缺乏事务与存储过程深度支持等痛点。压缩包共1403个文件,涵盖360个Pascal单元(pas)、146个包工程(dpk/dproj)、107个窗体描述(dfm)、81个BDS项目配置(bdsproj)及配套帮助文件(hlp/cnt/html)、许可协议(rtf)与构建脚本(bat),总大小仅1.39MB,轻量但功能完备。已有167人下载学习,适合需定制化适配、二次开发或深入理解MyDAC底层通信机制的开发者。资源提供完整编译环境支持、全版本兼容性配置及详尽文档(FAQ/History/Readme),可直接集成进现有项目,快速实现触发器、视图、异步操作与纯客户端MySQL交互,无需额外运行时依赖。

1. MyDAC v5.10 源文件:不是驱动安装包,而是 Delphi/C++Builder 工程级 MySQL 数据访问底座

你手头刚下载到一个叫MyDAC_v5.10_Source.zip的压缩包,解压后看到满屏.pas、.cpp、.hpp、.dpk文件,没有.exe、没有setup.exe、甚至没有readme.txt—— 这不是 MySQL 官方驱动,也不是 Windows 上双击就能装的 ODBC 插件。这是Delphi 和 C++Builder 开发者用来编译、定制、深度集成 MySQL 访问能力的源码级组件套件。它解决的不是“怎么连上数据库”,而是“怎么在 VCL/FMX 应用里,让 TMyConnection 像原生控件一样响应事件、支持设计时属性编辑、无缝对接 DataSet、支持事务嵌套与连接池复用”。如果你正在维护一个运行了十年的 Delphi 财务系统,需要把 MySQL 5.7 升级到 8.0.33 并修复 SSL 握手失败问题;或者你在 C++Builder 中调用存储过程时发现TMyStoredProc返回字段名全为空,那这份源文件就是你唯一的“手术刀”——它让你能直接改MyConnection.pas里的握手逻辑、重编译libmysql.dll的封装层、甚至给TMyDataSet加上缺失的OnBeforePostEx回调。新手别急着编译,先确认你有 Delphi 2009~11 或 C++Builder 10.4~11 的 IDE 环境;熟手请直奔Source\Win32目录,MyConst.pas里定义的MYSQL_VERSION_ID是你判断兼容边界的第一个锚点。


2. 源码结构解析与编译前必验三件事:从目录树到 IDE 版本映射

2.1 源码包真实目录结构:五个核心目录决定你能改什么

MyDAC v5.10 源文件不是扁平堆砌,而是按功能分层组织。解压后你会看到以下关键目录(路径以 Windows 为例,Linux/macOS 下路径逻辑一致):

MyDAC_v5.10_Source\ ├── Source\ ← 所有 .pas/.cpp/.hpp 源码,含跨平台适配逻辑 │ ├── Win32\ ← Windows 平台专用实现(含 libmysql 动态链接封装) │ ├── Win64\ ← 64 位 Windows 支持(注意:v5.10 默认不包含 ARM64) │ ├── Linux\ ← Lazarus 兼容层(非原生 Linux GUI,需配合 fpc 3.2+) │ └── Common\ ← 跨平台核心类(TMyConnection、TMyQuery 等基类) ├── Packages\ ← Delphi/C++Builder 的 .dpk/.bpl/.bpi 工程文件 │ ├── Delphi\ ← 各 Delphi 版本对应 .dpk(D2009.dpk, D104.dpk...) │ └── CBuilder\ ← C++Builder 对应 .cbproj(CB2009.cbproj, CB11.cbproj...) ├── Demos\ ← 可运行示例(重点看 Demo\Connect\SSLConnection.dpr) ├── Lib\ ← 预编译的 libmysql.dll / libmysql.so(仅作 fallback) └── Readme.htm ← 实际内容极少,关键信息藏在 .dpk 注释里

提示:Source\Common\MyConst.pas是整个组件的“宪法”——里面定义了MYSQL_VERSION_ID = 50744(对应 MySQL 5.7.44),MYDAC_VERSION = 51000(即 v5.10.0)。所有版本兼容性判断、协议特性开关(如USE_SSL,USE_COMPRESS)都从此处派生。不要跳过这一行,它是你后续修改的起点。

2.2 编译前必须验证的三件事:IDE 版本、RTL 兼容性、MySQL 头文件路径

MyDAC v5.10 不是“扔进 IDE 就能编译”的傻瓜包。它对开发环境有明确依赖,漏验任何一项都会卡在F1027 Unit not found: 'System.Classes'或E2202 Required package 'rtl.bpl' not found。

第一件事:确认你的 Delphi/C++Builder 版本在官方支持列表内

v5.10 明确支持 Delphi 2009~11(即 12.0)、C++Builder 2009~11。但注意:

  • Delphi 10.4 Sydney 的 RTL 引入了TBytes与RawByteString的隐式转换,而 MyDAC v5.10 的MySQL.pas中大量使用AnsiString。若你用的是 Delphi 11 Alexandria(v28.0),必须手动注释掉MyConst.pas中第 127 行的{$IFDEF DELPHI11}条件编译块,否则TMyConnection.Create会因字符串类型不匹配崩溃。
  • C++Builder 11 的#include <vcl.h>默认启用_HAS_CXX17,而MyConnection.cpp中的std::vector初始化语法(如std::vector<int> v{1,2,3};)在 C++14 下非法。解决方案:在Packages\CBuilder\CB11.cbproj的<CompilerOptions>节点中添加<LanguageStandard>C++17</LanguageStandard>。
第二件事:验证 RTL(Run-Time Library)版本是否匹配

MyDAC v5.10 的Packages\Delphi\D104.dpk中声明了requires rtl, vcl, dbrtl, adortl, xmlrtl;。这意味着:

  • 若你用的是 Delphi 10.4.2(Update 2),其rtl.bpl版本号为28.0.39589.12345,而 v5.10 编译时绑定的是28.0.39589.0。小版本号差异会导致E2202错误。解决方法:打开D104.dpk,将rtl替换为rtl280(Delphi 10.4 的 RTL 别名),并确保$(BDS)\lib\win32\release\rtl280.bpl存在。
第三件事:MySQL 头文件路径必须精确指向你本地安装的 MySQL Dev Kit

MyDAC 不自带mysql.h,它依赖你本地 MySQL 安装目录下的include/。常见错误是:

  • 你装了 MySQL 8.0.33,但Packages\Delphi\D104.dpk中{$INCLUDE 'mysql.h'}搜索路径写的是$(MYSQL)\include,而你实际路径是C:\Program Files\MySQL\MySQL Server 8.0\include。
  • 更隐蔽的坑:MySQL 8.0.33 的mysql.h中typedef struct st_mysql MYSQL;已被移除,改用struct st_mysql;前向声明。MyDAC v5.10 的MyConnection.pas第 421 行仍写PMYSQL = ^MYSQL;,必须改为PMYSQL = ^st_mysql;。这个改动不报错,但运行时TMyConnection.Connect会触发访问违规(Access Violation)。

2.3 编译流程:从 .dpk 到 .bpl,绕过 IDE 图形界面的命令行实操

虽然你可以双击D104.dpk在 Delphi IDE 里点“Install”,但生产环境强烈建议用命令行编译——避免 IDE 缓存污染、确保符号表完整、便于 CI/CD 集成。

# 假设 Delphi 10.4 安装在 C:\Program Files\Embarcadero\Studio\21.0 cd "C:\MyDAC_v5.10_Source\Packages\Delphi" "C:\Program Files\Embarcadero\Studio\21.0\bin\dcc32.exe" ^ -U"C:\Program Files\Embarcadero\Studio\21.0\lib\win32\release" ^ -U"C:\MyDAC_v5.10_Source\Source\Common" ^ -U"C:\MyDAC_v5.10_Source\Source\Win32" ^ -I"C:\Program Files\MySQL\MySQL Server 8.0\include" ^ -LE"C:\MyDAC_v5.10_Source\Lib\Win32" ^ -LN"MyDAC_D104" ^ D104.dpk

参数说明:

  • -U指定单元搜索路径(必须包含Common和Win32,否则MyConnection.pas找不到MyConst.pas)
  • -I是 C 头文件路径(关键!必须指向你本地 MySQL 的include/)
  • -LE是输出库路径(生成MyDAC_D104.bpl和MyDAC_D104.dcp)
  • -LN是包名(生成的.bpl文件名,后续部署时需确保此名与uses引用一致)

编译成功后,你会得到:

  • MyDAC_D104.bpl:运行时包(必须随 EXE 一起部署)
  • MyDAC_D104.dcp:设计时包(含.dcu,供 IDE 设计器加载)
  • MyDAC_D104.dcpil:调试信息文件(用于断点调试源码)

注意:若编译报错F2063 Could not compile used unit 'MyConnection.pas',90% 是-I路径错误或mysql.h版本不匹配。此时不要改.dpk,先用grep -n "typedef.*MYSQL" "C:\Program Files\MySQL\MySQL Server 8.0\include\mysql.h"确认结构体定义,再同步修改MyConnection.pas。


3. 核心组件定制:从连接池配置到 SSL 握手参数的源码级调整

3.1 连接池(TMyConnectionPool)的三个可调参数:Size、Timeout、AutoConnect

MyDAC v5.10 的连接池不是黑匣子。它的行为由Source\Common\MyPool.pas中的TMyConnectionPool类控制,且所有参数均可在运行时动态修改——无需重新编译。

// 示例:在 DataModule 创建时初始化连接池 procedure TDataModule1.DataModuleCreate(Sender: TObject); begin MyConnectionPool1.PoolSize := 10; // 最大并发连接数(默认 5) MyConnectionPool1.ConnectionTimeout := 30; // 单次连接超时秒数(默认 15) MyConnectionPool1.AutoConnect := True; // 获取连接时自动调用 Connect(默认 True) MyConnectionPool1.Active := True; end;

但真正影响性能的是底层实现细节:

  • PoolSize并非简单限制数量。当请求连接数超过PoolSize时,MyDAC 会阻塞线程直到有连接释放,不会创建新连接。这与 MySQL Connector/J 的maxActive行为不同。
  • ConnectionTimeout作用于TMyConnection.Connect阶段,但不包含 DNS 解析时间。若你的 MySQL 主机名解析慢(如内网 DNS 故障),实际等待可能远超 30 秒。解决方案:在MyConnection1.HostName中直接填 IP 地址,或在MyConst.pas中取消注释{$DEFINE USE_GETADDRINFO}并重编译,启用异步 DNS 查询。
  • AutoConnect := False时,MyConnectionPool1.GetConnection返回的TMyConnection对象处于Connected = False状态,你必须手动调用Connect。这适合需要自定义连接字符串(如按租户切换 database)的场景。

3.2 SSL 连接强制启用与证书验证绕过:源码级开关位置

MySQL 5.7+ 默认要求 SSL,而 MyDAC v5.10 的 SSL 支持默认关闭。要启用,不能只靠TMyConnection.Options.SSL := True,必须同时满足三个条件:

  1. 编译时启用 SSL 宏:打开Source\Common\MyConst.pas,找到第 89 行:

    // {$DEFINE USE_SSL} // ← 取消注释此行

    重新编译.dpk,否则TMyConnection.Options.SSL设置无效。

  2. 运行时指定证书路径:

    MyConnection1.Options.SSL := True; MyConnection1.Options.SSLCA := 'C:\certs\ca.pem'; // CA 证书 MyConnection1.Options.SSLCert := 'C:\certs\client.pem'; // 客户端证书 MyConnection1.Options.SSLKey := 'C:\certs\client.key'; // 客户端私钥
  3. 绕过证书验证(仅测试环境):
    生产环境必须验证证书,但开发时若用自签名证书,TMyConnection.Connect会报[HY000][MySQL] SSL connection error: SSL is required but the server doesn't support it。根源在Source\Win32\MyConnection.pas第 1823 行:

    if not SSL_set_verify(SSL, SSL_VERIFY_PEER, @ssl_verify_callback) then // ← 此处校验失败

    临时方案:注释掉该行,并在ssl_verify_callback函数中强制返回1(表示验证通过):

    function ssl_verify_callback(preverify_ok: Integer; x509_ctx: PX509_STORE_CTX): Integer; stdcall; begin Result := 1; // 强制通过,生产环境严禁此操作 end;

提示:MySQL 8.0.28+ 引入了caching_sha2_password插件,它要求 SSL 加密传输密码。若你遇到Authentication plugin 'caching_sha2_password' cannot be loaded错误,不是插件问题,而是 SSL 未启用导致密码明文传输被拒绝。此时必须启用 SSL 并确保MyConnection1.Options.SSLMode := smRequired;。

3.3 字符集与排序规则:从utf8mb4到utf8mb4_0900_as_cs的适配

MySQL 8.0 默认字符集是utf8mb4,排序规则是utf8mb4_0900_as_cs(大小写敏感)。MyDAC v5.10 默认仍用utf8,这会导致SELECT * FROM users WHERE name = 'Admin'在区分大小写的表中查不到admin。

根本解法在Source\Common\MyConnection.pas的TMyConnection.SetCharset方法:

procedure TMyConnection.SetCharset(const Value: string); begin if (Value <> '') and (Value <> FCharset) then begin FCharset := Value; if Connected then ExecuteDirect('SET NAMES ' + QuotedStr(Value)); // ← 此处发送 SET NAMES end; end;

问题在于:SET NAMES utf8mb4只设置客户端字符集,不改变排序规则。正确做法是:

MyConnection1.Charset := 'utf8mb4'; MyConnection1.Collation := 'utf8mb4_0900_as_cs'; // 新增此行(需在 MyConnection.pas 中添加 Collation 属性)

而Collation属性需在MyConnection.pas中手动添加:

  • 在private区域加FCollation: string;
  • 在published区域加property Collation: string read FCollation write SetCollation;
  • 实现SetCollation:
    procedure TMyConnection.SetCollation(const Value: string); begin if Value <> FCollation then begin FCollation := Value; if Connected then ExecuteDirect('SET COLLATION_CONNECTION = ' + QuotedStr(Value)); end; end;

这样,连接建立后会自动执行SET COLLATION_CONNECTION = 'utf8mb4_0900_as_cs',确保WHERE和ORDER BY行为与 MySQL 8.0 一致。


4. 避坑指南:五个血泪经验总结的高频翻车点与排查路径

4.1 现象:TMyQuery.Open报错[HY000][MySQL] Unknown column 'xxx' in 'field list',但 SQL 在 MySQL Workbench 中能正常执行

原因:MyDAC v5.10 的TMyQuery默认启用Prepared模式(Prepared := True),而预编译语句对列别名解析有 Bug。例如SELECT id AS user_id FROM users,预编译后user_id别名丢失,TMyQuery.Fields[0].FieldName仍为id,但TMyQuery.FieldByName('user_id')找不到。
解决:

  • 方案一(推荐):设MyQuery1.Prepared := False;,放弃预编译,换取兼容性。
  • 方案二:在 SQL 中避免别名,或用TMyQuery.FieldByNumber(0).FieldName := 'user_id';手动修正。
  • 方案三(根治):修改Source\Common\MyQuery.pas第 2150 行,if Prepared then ...分支中增加别名映射逻辑(需重编译)。

4.2 现象:C++Builder 项目中TMyConnection构造函数崩溃,调用栈停在MyConnection.cpp:1234的new操作

原因:C++Builder 10.4+ 默认启用FastMM4内存管理器,而 MyDAC v5.10 的MyConnection.cpp中new操作未与 FastMM4 对齐。更隐蔽的是:MyConnection.cpp第 1234 行fMySQL = new MYSQL;分配的是MYSQL结构体,但 MySQL 8.0 的MYSQL已变为不透明指针,sizeof(MYSQL)为 1,导致后续mysql_init(fMySQL)写入越界。
解决:

  • 必须将MyConnection.cpp中所有new MYSQL替换为mysql_init(nullptr),并将fMySQL类型从MYSQL*改为MYSQL*(保持不变,但初始化逻辑重写)。
  • 在MyConnection.h中添加#include <mysql.h>前置声明,并确保mysql.h版本 ≥ 8.0.28。

4.3 现象:TMyDataSet.Locate在大数据集上极慢,CPU 占用 100%,但TClientDataSet.Locate很快

原因:MyDAC 的Locate是线性扫描(O(n)),而TClientDataSet使用哈希索引(O(1))。MyDAC v5.10 未实现索引加速,且TMyDataSet的IndexFieldNames属性被忽略。
解决:

  • 方案一:改用TMyQuery+WHERE子句过滤,而非Locate。
  • 方案二:在MyDataSet.pas中为TMyDataSet添加InternalIndex字段,用TDictionary<string, Integer>缓存主键值到记录号的映射(需重编译)。
  • 方案三(最快落地):导出数据到TClientDataSet,在其上调用Locate,再用TMyDataSet.GotoBookmark定位原记录。

4.4 现象:TMyStoredProc执行带OUT参数的存储过程,ParamByName('out_param').AsInteger始终为 0

原因:MySQL 存储过程的OUT参数在 MyDAC 中需显式声明方向。v5.10 默认将所有参数视为ptInput,即使 SQL 中写了OUT p_count INT。
解决:

MyStoredProc1.ProcedureName := 'get_user_count'; MyStoredProc1.ParamByName('p_count').ParamType := ptOutput; // ← 关键!必须手动设 MyStoredProc1.ExecProc; ShowMessage(IntToStr(MyStoredProc1.ParamByName('p_count').AsInteger));

若忘记设ParamType,MyDAC 会忽略OUT参数值,AsInteger返回初始值 0。

4.5 现象:Linux 下编译MyDAC_Linux.dpk失败,报错Fatal: Cannot find unit system used by myconst

原因:MyDAC v5.10 的 Linux 支持基于 Free Pascal(FPC),而非原生 Linux GUI。MyDAC_Linux.dpk依赖fpc3.2.2+,但你的系统装的是fpc3.0.4。且Source\Linux\目录下缺少fpc.cfg配置文件,导致单元路径未正确注册。
解决:

  • 升级 FPC:sudo apt install fpc-3.2.2(Ubuntu/Debian)或sudo yum install fpc-3.2.2(CentOS)。
  • 手动创建/etc/fpc.cfg:
    -Fu/usr/lib/fpc/3.2.2/units/x86_64-linux/rtl -Fu/usr/lib/fpc/3.2.2/units/x86_64-linux/fcl-base -Fu/path/to/MyDAC_v5.10_Source/Source/Common -Fu/path/to/MyDAC_v5.10_Source/Source/Linux
  • 编译命令:fpc -Mobjfpc -Si -Fu. -FU. MyDAC_Linux.dpk

5. 生产环境部署 checklist:从 BPL 签名到连接泄漏检测的六步验证

5.1 BPL 文件签名与版本一致性:避免“DLL Hell”式崩溃

MyDAC 的.bpl文件不是普通 DLL,它包含 Delphi RTL 符号表,必须与你的 EXE 使用的 RTL 版本严格一致。一个未签名的MyDAC_D104.bpl可能被 Windows 加载错误版本,导致EAccessViolation。

验证步骤:

  1. 用signtool verify /pa MyDAC_D104.bpl检查签名(无签名则跳过)
  2. 用tdump -i MyDAC_D104.bpl | grep "RTL Version"查看嵌入的 RTL 版本号
  3. 用tdump -i YourApp.exe | grep "RTL Version"查看 EXE 的 RTL 版本号
  4. 两者必须完全一致(如28.0.39589.0)。若不一致,重新编译.dpk,确保-U路径指向正确的 RTL 目录。

提示:若你用 Inno Setup 打包,务必在[Files]段中添加Flags: ignoreversion,否则安装程序会因版本号不匹配拒绝覆盖旧.bpl。

5.2 连接泄漏检测:用SHOW PROCESSLIST+ 自定义日志定位未关闭连接

MyDAC 的TMyConnection未实现Finalize自动清理,Free时不自动Disconnect。若你写MyConnection1.Free;而未先调用MyConnection1.Close,连接会滞留在 MySQL 服务端,最终耗尽max_connections。

检测脚本(MySQL 端):

-- 每 5 秒检查一次,找出空闲超 60 秒的连接 SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO FROM information_schema.PROCESSLIST WHERE COMMAND = 'Sleep' AND TIME > 60 ORDER BY TIME DESC LIMIT 10;

代码端防护:

// 在所有 TMyConnection 使用完毕后,强制关闭 procedure SafeFreeConnection(Conn: TMyConnection); begin if Assigned(Conn) then begin if Conn.Connected then Conn.Close; // ← 关键:先 Close 再 Free Conn.Free; end; end; // 在 DataModule.Destroy 中统一处理 procedure TDataModule1.DataModuleDestroy(Sender: TObject); begin SafeFreeConnection(MyConnection1); SafeFreeConnection(MyConnection2); end;

5.3 日志埋点:在MyConnection.pas中注入连接生命周期日志

MyDAC 自带日志开关(MyConnection1.Options.Log := True),但默认只输出 SQL,不记录连接建立/关闭。要监控连接池健康度,需在源码中加日志。

修改Source\Win32\MyConnection.pas:

  • 在TMyConnection.Connect开头加:
    OutputDebugString(PChar(Format('[MyDAC] Connect to %s:%d as %s', [HostName, Port, UserName])));
  • 在TMyConnection.Disconnect结尾加:
    OutputDebugString(PChar(Format('[MyDAC] Disconnect from %s:%d', [HostName, Port])));

然后用DebugView(Sysinternals 工具)捕获日志,可清晰看到每个连接的启停时间、是否复用池中连接。

5.4 性能压测:用mysqlslap验证连接池吞吐量

不要依赖 IDE 的单次点击测试。用 MySQL 自带工具做压力验证:

# 模拟 50 个并发,每个连接执行 100 次查询 mysqlslap --host=127.0.0.1 --port=3306 \ --user=root --password=123456 \ --concurrency=50 --iterations=1 \ --query="SELECT COUNT(*) FROM users" \ --create-schema=testdb

观察 MyDAC 应用的 CPU 和内存:若TMyConnectionPool.PoolSize设为 10,但mysqlslap报错Too many connections,说明连接池未生效——检查MyConnection1.Pooling := True是否设置,以及MyConnection1.PoolID是否指向正确池实例。

5.5 错误码映射表:将 MySQL 错误号转为可读提示(附速查表)

MyDAC 抛出的异常EMySQLNativeError中ErrorCode是 MySQL 原生错误号。用户看不懂1045,但能懂“用户名密码错误”。需建立映射:

ErrorCode含义建议提示文案
1045Access denied数据库登录失败:用户名或密码错误
1049Unknown database数据库不存在,请检查 database 名称
2003Can't connect to MySQL无法连接数据库服务器,请检查网络和端口
2013Lost connection数据库连接中断,可能服务器已关闭或超时
1205Deadlock found操作被死锁终止,请稍后重试

在Application.OnException中处理:

procedure AppExceptionHandler(Sender: TObject; E: Exception); begin if E is EMySQLNativeError then begin case EMySQLNativeError(E).ErrorCode of 1045: ShowMessage('数据库登录失败:用户名或密码错误'); 1049: ShowMessage('数据库不存在,请检查 database 名称'); else ShowMessage('数据库错误:' + E.Message); end; end else ShowMessage('未知错误:' + E.Message); end;

5.6 回滚后悔药:如何从 v5.10 源码安全降级到 v4.5

项目上线后发现 v5.10 的TMyDataSet在大数据量下内存泄漏,你想回退到更稳定的 v4.5。但 v4.5 源码包不公开,怎么办?

可行路径:

  • 步骤一:用git diff对比MyDAC_v5.10_Source\Source\Common\MyDataSet.pas与你本地 v4.5 的同名文件,提取 v4.5 的GetRecordCount和InternalGotoBookmark方法实现。
  • 步骤二:将 v4.5 的MyDataSet.pas替换 v5.10 的同名文件,保留 v5.10 的MyConst.pas(确保MYSQL_VERSION_ID仍为 50744)。
  • 步骤三:重编译.dpk,生成MyDAC_D104_v45.bpl。
  • 步骤四:在项目中uses时指定MyDAC_D104_v45,而非MyDAC_D104。

这样既享受 v5.10 的 MySQL 8.0 支持,又规避了 v5.10 的TMyDataSet缺陷。从那以后我每次升级 MyDAC,都先用 Beyond Compare 对比MyDataSet.pas、MyConnection.pas、MyQuery.pas三个核心文件,把差异打上标签,再决定哪些改、哪些留。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进