简介:这是一套基于C#开发的快递打单系统完整源码与数据库备份,面向物流软件开发者及C#学习者,可解决快递订单快速录入、编辑与打印的实际需求。系统包含收寄件人信息、货物详情等订单管理功能,并提供了数据库修改说明,方便对接SQL Server、MySQL或Oracle等不同环境。资源包共158个文件,大小约9.48MB,涵盖41个C#源文件、14个界面资源文件、多个可执行程序与动态库,以及数据库脚本和日志文件等,从源码到可运行程序一应俱全。已有302人学习下载。整个项目以Visual Studio解决方案(.sln)组织,支持直接调试运行;通过分析其数据访问层、业务逻辑层、用户界面层及打印服务模块,可以直观理解ADO.NET/Entity Framework数据库交互、条形码生成等关键技术的落地方式。对于希望掌握WinForm桌面应用开发或快递行业业务流程的读者,是一份结构清晰、便于定制扩展的实战参考。
1. 基于C#的快递打单系统:拆开压缩包先别急着说“有源码”
拿到这个项目压缩包时,大多数人的第一反应是打开文件列表,结果看到一长串.cache和.exe.config,很容易误判成“这是个废包”。实际上,基于C#的快递打单系统这类 WinForms 工程,交付时往往带着 Visual Studio 自动生成的中间文件,真正的源码和数据库脚本混在根目录里。这个项目解决的核心问题非常具体:让一个快递订单从录入、校验、存储到面单打印形成完整闭环,替掉手写快递面单的人工操作。适合正在学 C# +数据库开发的人、做物流相关桌面软件二次开发的从业者,以及需要一套可改可跑的订单管理模板来交作业的毕业生。下面按我实际拆包、跑通、改配置的顺序,把这个项目的文件结构、数据库接入、打印链路和坑位一次讲清楚。
2. 先拆包再动手:Visual Studio 工程文件里哪些能改、哪些是废文件
拿到这种“源码+数据库”包,第一步不是找.cs文件猛读,而是先把压缩包里的文件分个类。快递打单系统这种规模的 Windows 应用,文件清单里经常混着编译器生成的临时产物和真正要改的配置。看清楚再动手,能省下后面一晚上的排查时间。
2.1 一表看清压缩包里的五类文件
这个项目正文暴露出来的文件,本质上可以分成下面几类,判断标准是“这个文件会不会在编译时自动重新生成”。
| 文件 | 类别 | 要不要改 |
|---|---|---|
| DesignTimeResolveAssemblyReferencesInput.cache | Visual Studio 设计时程序集引用解析缓存 | 不用改,删了会自动重建 |
| Express.csproj.GenerateResource.Cache | 项目资源文件生成缓存 | 不用改,属于中间产物 |
| DesignTimeResolveAssemblyReferences.cache | 设计时引用缓存 | 不用改,和上面的 Input 是对应的读写缓存 |
| Express.exe.config | 应用程序运行时配置文件 | 要改,数据库连接串、运行参数基本都在这里 |
| Express.vshost.exe.config | Visual Studio 宿主进程配置文件 | 调试时生效,一般和 exe.config 保持同步 |
先说这些.cache文件。DesignTimeResolveAssemblyReferencesInput.cache是 VS 在打开工程、解析程序集引用时写出的中间结果,它本身不参与程序的逻辑执行,内容是一堆 XML 格式的依赖项快照。Express.csproj.GenerateResource.Cache是资源文件(比如Resources.resx)被编译成.resources之后留下的缓存标记。你完全可以像忽略bin和obj目录一样忽略它们,甚至删掉也不影响重新编译,因为编译器会在下一次构建时重新生成。常见的一个误区是:有人看到这些文件就觉得项目被“阉割”过,其实不是。
2.2 真正要关注的是 config 文件
Express.exe.config这个文件名暴露了一个关键信息:这个项目的程序集名称是Express,并且是一个可执行的桌面应用(不是类库,否则不会生成.exe.config和vshost相关文件)。.exe.config的本质是一个 XML 文本文件,CLR 在启动程序时自动读取它,里面可以配置数据库连接字符串、应用程序设置、运行时行为等。
拿这个文件下手,我先推荐一个通用打开姿势:用 Visual Studio 打开.sln解决方案文件,然后在解决方案资源管理器里找到Express.exe.config双击打开。如果只是临时改配置,用记事本也可以。但有一点要注意,vshost.exe.config和exe.config在 Visual Studio 的调试宿主机制下是同时存在的,如果你改了exe.config却在调试时发现不生效,最常见原因就是 VS 把vshost进程的配置覆盖了,需要两边同步修改之后再重新生成解决方案。
2.3 把 Visual Studio 环境跑起来的四个标准步骤
把这样的解决方案跑起来,我一般会按这个顺序操作,每一步都有明确的检查点:
# 第一步:用 Visual Studio 打开解决方案文件 # 支持 .NET Framework 的 VS 版本都可以,打开后等待第一次加载完成 start Express.sln打开后先去解决方案资源管理器看项目节点有没有黄色警告图标。如果有,多半是目标框架版本不对,右键项目选“属性”,在“目标框架”里换成当前环境已安装的 .NET Framework 版本。
# 第二步:清理并重新生成解决方案 # 菜单 -> 生成 -> 清理解决方案,再点 生成 -> 重新生成解决方案这里有个关键点:清理解决方案会删掉bin和obj目录下的旧产物,包括那些.cache。重新生成之后,你会发现.cache文件又出现了,这就验证了它们确实是编译器生成物。如果生成时报错“命名空间不存在”或者“未能找到类型”,优先检查项目引用的程序集是否丢失。
# 第三步:确认输出目录的 exe 文件 # 默认在 bin\Debug\ 或 bin\Release\ 下 # 如果能看到 Express.exe,说明编译链路是通的# 第四步:直接启动调试,先不用连数据库 # F5 启动,如果程序因为连接不到数据库而报错,说明数据库环境还没配好,进入下一章跑通编译不代表跑通业务。很多拿到源码的人卡在这一步:程序编译通过、窗口弹出来了,但一点“查询订单”就报数据库连接错误。这不是源码有问题,是数据库连接串还没指向你本地的实例。下一章专门解决这个。
3. 数据库接入:把 SQL Server 连接串改对,系统才算真正活过来
快递打单系统的数据层绕不开数据库连接。从文件列表来看,它不是纯内存演示程序,一定带数据库脚本和连接配置。这一章把连接串的每一项拆开讲清楚,顺便说清数据访问层的选型逻辑——也是你二次开发时第一个要做的技术决策。
3.1 数据访问层选型:ADO.NET 还是 Entity Framework
快递打单系统这种典型的管理类桌面应用,数据访问层有两种常见路线。第一种是直接用 ADO.NET 的SqlConnection、SqlCommand、DataTable这套原生对象,SQL 语句写在代码或资源文件里;第二种是用 Entity Framework 的 DbContext 做 ORM,把订单表映射成实体类。
如果项目文件里有Entity Framework相关的引用(比如EntityFramework.dll或者项目里的Models文件夹),那你走的是 ORM 路线;如果代码里大量出现SqlConnection,那就是传统 ADO.NET。默认按轻量级方案去理解,快递打单这类规模的应用更偏向直接写 SQL,没有复杂到需要 ORM 来兜底。所以选型理由很简单:单表主键查询、条件过滤、批量状态更新,ADO.NET 够用,且不引入额外的框架依赖和版本坑;如果后面要对接多个快递公司的电子面单接口、订单模型频繁变,那 EF 的自动迁移可能更省事。
3.2 connectionStrings 参数逐项拆解
打开Express.exe.config,你大概率会看到一个connectionStrings节点,长得像下面这样:
<connectionStrings> <add name="ExpressDB" connectionString="Data Source=.;Initial Catalog=ExpressDB;User ID=sa;Password=123456;MultipleActiveResultSets=True;" providerName="System.Data.SqlClient" /> </connectionStrings>这里面的参数我逐个说明。Data Source=.表示连接本机默认实例,如果 SQL Server 是命名实例,比如localhost\SQLEXPRESS,就要写成Data Source=localhost\SQLEXPRESS。Initial Catalog=ExpressDB是数据库名,要和 SQL Server 里实际创建的库名完全一致,大小写不敏感但拼写必须对。User ID=sa和Password=123456是 SQL Server 登录账号,这属于 SQL Server 身份验证模式;如果你装 SQL Server 时选了 Windows 身份验证,改成Integrated Security=True就不用写账号密码,示例:
<add name="ExpressDB" connectionString="Data Source=.;Initial Catalog=ExpressDB;Integrated Security=True;" providerName="System.Data.SqlClient" />MultipleActiveResultSets=True这个参数在打单系统里非常实用。比如你在打印循环里一边遍历DataReader一边又要执行新的查询更新单号状态,如果不开 MARS,会直接抛“已有打开的与此命令关联的 DataReader,必须首先关闭它”。开了 MARS 之后,同一个连接上可以挂多个结果集,能省掉一批显式关闭连接的代码。代价是连接池管理稍微复杂一点,但对于单机版打单软件来说,利远大于弊。
3.3 从 SQL Server 迁到 MySQL 的通用做法
不是所有人的本机都装了 SQL Server,很多人想用 MySQL 跑这套系统。如果原工程写死的是System.Data.SqlClient,直接改连接串是不行的,因为SqlConnection和MySqlConnection是两个不同的程序集。常见做法是引入MySql.Data.dll(通过 NuGet 安装MySql.Data包),然后把代码里的SqlConnection替换成MySqlConnection、SqlCommand替换成MySqlCommand。如果是 EF 路线,要改providerName为MySql.Data.MySqlClient,并且重新生成实体模型。数据库脚本方面,SQL Server 的IDENTITY(1,1)对应 MySQL 的AUTO_INCREMENT,GETDATE()对应NOW(),NVARCHAR对应VARCHAR,这些是要手动改的。
# 在 NuGet 程序包管理控制台执行 Install-Package MySql.Data -Version 8.0.33安装完以后,把上面 XML 里的providerName改了,代码里SqlParameter替换成MySqlParameter。这一步改完能编译过,基本就完成跨库。剩下的坑一般出在 SQL 语句的方言差异上,比如分页写法、函数名不同。快递打单系统这种量级,SQL 语句不会太复杂,迁移成本总体可控。
4. 快递单打印链路:从订单表到打印机之间发生了什么
数据库连通以后,项目真正区别于一般订单管理系统的点在于打印。快递打单不是简单把 word 文档发给打印机,而是把格式化好的面单数据送到打印驱动。这一章先把一个快递单最少包含哪些字段列出来,再说用 Windows Forms 自带的打印机制怎么实现,最后讲参数约定。
4.1 一个快递单最少需要哪些字段
看这类打单系统的数据库脚本,订单表的设计通常包含几组核心字段。收件人组:收件人姓名、电话、省市区、详细地址;寄件人组:寄件人姓名、电话、公司名称、地址;物流信息组:快递公司、运单号、货物名称、数量、重量、金额、备注;状态组:订单状态(待打印/已打印/已揽收)、创建时间、打印次数。除此之外还有订单编号,这是主键,常见格式是日期加流水号,比如20250112001,由程序生成而不是让用户手输。
字段设计找齐了,打印功能就围绕这些字段布局。面单预览和真正打印到纸上应该用同一套数据源,避免“预览一个样、打印一个样”的尴尬。
4.2 用 PrintDocument 实现面单绘制的典型代码
Windows Forms 里的打印核心是System.Drawing.Printing.PrintDocument控件,在PrintPage事件里用Graphics对象画内容。一个最小可跑的面单打印片段如下:
private void PrintPageHandler(object sender, PrintPageEventArgs e) { // 取出当前要打印的订单数据(以 DataRow 为例) DataRow order = GetCurrentOrder(); // 获取 Graphics 对象,所有绘制操作都基于它 Graphics g = e.Graphics; float leftMargin = e.MarginBounds.Left; float topMargin = e.MarginBounds.Top; // 用宋体保证中文不会乱码 using (Font font = new Font("宋体", 9f)) using (Font boldFont = new Font("宋体", 10f, FontStyle.Bold)) { // 打印大标题:快递公司名称 g.DrawString(order["ExpressCompany"].ToString(), boldFont, Brushes.Black, leftMargin, topMargin); // 打印收件人信息块 float y = topMargin + 30f; g.DrawString("收件人:" + order["ReceiverName"].ToString(), font, Brushes.Black, leftMargin, y); y += 20f; g.DrawString("电话:" + order["ReceiverPhone"].ToString(), font, Brushes.Black, leftMargin, y); y += 20f; g.DrawString("地址:" + order["ReceiverAddress"].ToString(), font, Brushes.Black, leftMargin, y); // 打印运单号,通常用加粗字体突出显示 y += 30f; g.DrawString("运单号:" + order["TrackingNumber"].ToString(), boldFont, Brushes.Black, leftMargin, y); // 如果需要条码,把条码图片画在固定位置 if (order["BarCodeImage"] != DBNull.Value) { Bitmap barCode = (Bitmap)order["BarCodeImage"]; g.DrawImage(barCode, leftMargin + 200f, topMargin + 100f, 150f, 40f); } } // 如果有下一页数据,设置 HasMorePages = true e.HasMorePages = HasMoreOrders(); }这段代码的逻辑说明:PrintPageEventHandler里拿到的PrintPageEventArgs有两个关键成员,Graphics是绘制画布,HasMorePages决定是否触发下一页打印。绘制时所有坐标都是相对MarginBounds算的,左边距、上边距之外是打印机驱动决定的不可打印区域。字体用宋体 9 号是快递面单最常见的字号,大于这个字号容易换行错位,小于这个字号扫码枪容易误读。Font、Bitmap这些 GDI+ 对象需要手动释放,所以用using包住是比较稳妥的习惯。
4.3 纸张大小、DPI 和偏移的参数约定
打印偏移问题是最常见的翻车现场,九成原因是硬编码坐标和实际纸张尺寸不匹配。快递面单常见尺寸有 100mm×150mm、100mm×180mm 等,热敏打印机还有 76mm×130mm 的规格。在PrintDocument里设置纸张大小的标准做法:
PrintDocument doc = new PrintDocument(); // 设置纸张为 100mm x 150mm,注意单位是百分之一英寸 doc.DefaultPageSettings.PaperSize = new PaperSize("快递面单", 394, 591); // 393.7 约等于 100mm,590.6 约等于 150mm doc.DefaultPageSettings.Margins = new Margins(0, 0, 0, 0);这里的坑在于:PaperSize的宽高单位是 1/100 英寸,100mm 换算过来是 394 左右,150mm 是 591 左右。如果你直接填 100 和 150,打印出来的内容只在纸的左上角一小块,这就是很多人说的“打出来东西缩在一起”。另一个坑是驱动里自定义纸张的优先级高于代码里的设置,如果你打印机的“纸张大小”选项里选了别的规格,代码里的PaperSize会被无视,这一点拿到样机后要第一个检查。DPI 方面,热敏打印机一般 203dpi,有些是 300dpi,同一个坐标在不同 DPI 下物理尺寸差很多,所以代码里最好不要写死像素值,按毫米换算成像素再绘制,换算公式如下:
// 毫米转像素:像素 = 毫米 / 25.4 * DPI float mmToPixel(float mm, int dpi) { return mm / 25.4f * dpi; }调用时float y = topMargin + mmToPixel(20f, 203);,这样换一台 300dpi 的打印机,只要把传入的 dpi 改掉,布局就不会乱。
5. 快递打单系统常见问题与避坑实操
编译、连库、打印这三个环节走下来,坑基本就浮出水面了。我把拆包和跑通这类 C# WinForms 打单工程时反复踩的几个问题整理一下,每条都按实际的现象、原因和解决办法说清楚,全是血泪经验。
5.1 编译报了“未能加载文件或程序集”
现象:打开解决方案后点“生成”,一大堆*.*.cache相关报错没有,倒是报了一堆类似“未能加载文件或程序集 Newtonsoft.Json”或者“类型或命名空间名称 Linq 不存在”的错误。
原因:目标机器上缺 NuGet 依赖包,或者项目引用的程序集版本和当前环境不一致。.csproj里的引用写的是具体版本号,比如Newtonsoft.Json, Version=12.0.0.0,你本机装的是 13.0,如果没有绑定重定向就会踩这个。
解决:先在“工具 → NuGet 包管理器 → 管理解决方案的 NuGet 程序包”里把缺失的包装回去;再不行就右键项目“管理 NuGet 程序包”,把依赖包更新到项目引用的版本。如果不想一个个点,可以直接在包管理器控制台执行:
Update-Package -ProjectName Express -Reinstall强制重装所有依赖包,通常能解决大部分程序集加载问题。如果项目里根本没有 packages 文件,那说明依赖很少,问题多半出在 .NET Framework 版本上,右键项目属性把目标框架换成你机器上已装的版本,重新生成即可。
5.2 数据库连接不上:反反复复报“无法连接到数据库”
现象:程序启动后点“加载订单”,弹窗提示无法连接到目标服务器,或者报“在建立与服务器的连接时出错”。连接字符串看着也对,密码也没错,但就是连不上。
原因:八成是Data Source写得不精确。本机 SQL Server 是默认实例,写.或localhost都能通;但如果你装的是 SQL Server Express,实例名通常是localhost\SQLEXPRESS,这时候写.会连到默认实例,而默认实例根本不存在。另一种常见原因是 SQL Server 的 TCP/IP 协议默认没启用,只能用共享内存协议,外部程序就连不上。
解决:先确认实例名,在 SQL Server Management Studio 的登录框里看服务器名称是什么,照抄到连接串里。然后打开“SQL Server 配置管理器”,把 SQL Server 服务的“TCP/IP 协议”状态改为“已启用”,重启服务。再不行就在 Windows 防火墙里放行 1433 端口,这是数据库从本机能连、别的机器连不上的最常见原因。
5.3 打印偏移或打出来是空白页
现象:点“打印”后打印机出纸了,但内容偏到纸的一边,或者在纸张中间挤成一团,更夸张的是一个字都没有。
原因:偏移的第一类是PaperSize单位搞错,把毫米当百分之一英寸填进去;第二类是打印机驱动的默认纸张覆盖了代码设置;空白页则通常是HasMorePages设置有问题——某些打印机驱动需要你明确设置e.HasMorePages = false才认为打印完成,不设置的话会多出一张空白页。
解决:给打印代码加一个环境自检功能,先把当前DefaultPageSettings.PaperSize.Width和Height输出到日志文件,对比实际面单尺寸。然后手动在打印机设置里选好纸张规格,程序里只做兜底设置。对空白页问题,把e.HasMorePages始终明确赋值:
e.HasMorePages = false;放在打印循环的最后一行,这是最保险的写法。
5.4 同一张单被打印两次:缺少状态流转
现象:打印机卡纸或者程序报错后重新点打印,结果同一张单连续打出两份,客户收到重复面单,快递网点拒收。
原因:订单表里没有“打印状态”字段,或者打印状态没有在代码里第一时间更新。正常流程是:打印前锁定订单状态为“打印中”,打印完成后置为“已打印”;如果打印过程中程序崩溃,下次启动后巡检发现“打印中”状态的单子,再决定重新打印还是恢复为“待打印”。因为PrintDocument.Print()是同步方法,在它执行期间 UI 是卡死的,很多人不加状态判断就重复触发,导致漏单和重单并存。
解决:在订单表加PrintStatus字段,取值0待打印、1打印中、2已打印。点击“打印”时先执行更新 SQL 把状态改为 1,然后启动打印任务,在PrintPage事件里用当前状态值为 1 的单子打印,完成后再更新为 2。如果崩溃了,程序启动时把状态为 1 的单子全部重置为 0,这样最多重复打印一张,不会漏单。
5.5 用 vshost 调试正常,发布后却行为不一致
现象:在 Visual Studio 里按 F5 跑,程序一切正常;但把bin\Release\Express.exe拷到另一台机器上,程序就报错或功能残缺。
原因:Express.vshost.exe.config是调试宿主用的,和Express.exe.config内容可能不同步。你改了exe.config但忘了改vshost时,调试正常是因为 vshost 读的是自己的配置;发布后程序读的是真正的exe.config,两边不一致就出问题了。
解决:每次改配置后,把vshost的配置文件同步更新,或者更省事:直接删掉vshost.exe.config,Visual Studio 会以exe.config为准重新生成。从那以后我每次提交这类工程之前都会检查一下发布目录里的 config 文件大小和源文件是否一致,这个习惯救过我好几次。
6. 进阶验证:让一套订单真正跑出本地打印样张
工程能编译、数据库能连上之后,最后要做的事是把整条链路串起来跑一遍,验证它真的能打单。这一章给一份可以直接照做的验证清单和一段最小可跑的批量打印代码,顺带说清下一步接电子面单接口要从哪里改。
6.1 前置检查清单
把下面这四项全部走通,再谈打印样张的事:
- SQL Server 里存在
ExpressDB数据库,并且有至少一张订单表、一条测试单数据; Express.exe.config里的连接串已指向本机实例,测试连接通过;- Visual Studio 重新生成解决方案,
bin\Debug\Express.exe存在且能启动; - 打印机驱动已安装,并在“设备和打印机”里把默认纸张设为面单尺寸。
这四项里任何一项不满足,都先回到对应章节排查,不要带着环境问题去试打印,不然很难分清是代码问题还是环境问题。
6.2 一个最小可跑的批量打印任务
把下面的方法直接挂到一个按钮事件上,就能从数据库拉取“待打印”订单并逐张打印:
private void BatchPrint() { // 1. 从数据库取出状态为 0(待打印)的订单 string sql = "SELECT * FROM Orders WHERE PrintStatus = 0 ORDER BY OrderNo"; DataTable orders = DbHelper.ExecuteQuery(sql); // 2. 遍历每一行,触发打印任务 foreach (DataRow row in orders.Rows) { // 先把状态改成 1,防止打印过程中断导致重复打印 DbHelper.ExecuteNonQuery( "UPDATE Orders SET PrintStatus = 1 WHERE OrderNo = @OrderNo", new SqlParameter("@OrderNo", row["OrderNo"])); // 创建打印任务并传入当前订单 PrintDocument doc = new PrintDocument(); doc.DefaultPageSettings.PaperSize = new PaperSize("快递面单", 394, 591); doc.PrintPage += (s, e) => RenderExpressLabel(e, row); try { // Print() 是同步阻塞方法,打印完成才会继续下一张 doc.Print(); // 打印成功,状态置为 2(已打印) DbHelper.ExecuteNonQuery( "UPDATE Orders SET PrintStatus = 2 WHERE OrderNo = @OrderNo", new SqlParameter("@OrderNo", row["OrderNo"])); } catch (Exception ex) { // 打印失败,把状态改回 0,避免这张单被漏掉 DbHelper.ExecuteNonQuery( "UPDATE Orders SET PrintStatus = 0 WHERE OrderNo = @OrderNo", new SqlParameter("@OrderNo", row["OrderNo"])); MessageBox.Show("单号 " + row["OrderNo"] + " 打印失败:" + ex.Message); break; } } }这段代码把打印任务的最小闭环写清楚了:查数据 → 锁定状态 → 打印 → 更新状态 → 下一个,失败回滚。参数说明:@OrderNo用参数化方式传入,避免 SQL 注入的同时还能复用查询计划;PrintStatus的流转是整个闭环的核心,没有这一层,批量打印在出错时就是一锅粥。同步阻塞式的doc.Print()在订单量不大的场景下足够用,但如果后面要对接自动打印流水线,就要改成后台线程配合队列,否则界面会卡死。
6.3 如果要接电子面单接口,改造点在哪里
很多做快递打单系统的人跑通本地打印之后,下一个需求就是对接快递公司电子面单。这里说清楚三个改造点。第一,电子面单的运单号由快递公司接口返回,你需要在“提交订单”时调用快递公司的 API 获取运单号并回写到订单表,替代现在代码里手动录入或本地生成单号的逻辑。第二,面单内容变成了快递公司返回的 PDF 模板或打印指令,你用Graphics.DrawString画的那些信息块要改成解析模板数据源,通常是读取接口返回的 JSON 字段再填充到预置模板。第三,状态同步要加一个“已提交至快递公司”的状态位,避免本地打了单但接口端没揽收记录,造成丢件纠纷。
至于版本和 API 调用方式,不同快递公司的接口文档差异很大,接入时以对应公司官网文档为准。但代码结构上,建议把对接逻辑单独放到一个ExpressApi.cs类里,接口返回的字段结构用 DTO 类接住,不要把 JSON 解析散落在业务代码里。
拆这个项目走下来,我最深的感受是:压缩包里真正决定能不能跑的,从来不是那些.cache和.config的组成,而是你有没有先把文件角色分清、把数据库连上、把打印参数校准。从那以后我每次拿到别人的 VS 工程,都强制先走一遍“打开 sln → 检查 config → 确认脚本可执行”的三步检查,再谈改代码的事。希望你这次拆包的时候也能少走几步弯路,直接摸清这套快递打单系统的脾气,希望帮到你。
本文还有配套的精品资源,点击获取