简介基于C#的仓库管理系统毕业设计资料包面向需要完成课程设计或毕业设计的计算机专业学生以及希望快速搭建进销存类小型信息管理系统的开发者。系统覆盖货物入库、出库、调库等核心业务并包含仓库单位、货物类别、供货商、客户档案及操作员管理等模块后台采用SQL 2005数据库前端以Visual Studio 2005为开发工具具备数据添加、修改、保存、删除等完整处理流程。压缩包内含165个文件总大小约9.71MB主要文件包括53个C#窗体源代码文件、20个Windows窗体资源文件、5个Word论文文档以及项目工程文件和数据库文件便于对照代码与论文理解整体设计思路。已有1968人学习下载资料既提供可运行的完整项目也附带毕业论文Word材料可作为仓库管理系统设计的课程设计参考也适合在此基础上进行二次开发与论文撰写借鉴。1. C# 仓库管理系统一门课设源码里的桌面 MIS 完整套路C# 仓库管理系统这类源码项目在毕业设计和求职作品集里常年不缺热度但真正拆开看它的价值不在算法而在于把入库、出库、调库、基础档案这些真实业务用 WinForms 加 SQL Server 稳稳妥妥地串起来。这套基于 Visual Studio 2005 和 SQL 2005 的仓库管理信息系统核心是 WMSDataSet 类型化数据集加表单 CRUD覆盖货物入库、出库、调库以及供货商、客户、货物类别、操作员管理模块。刚学完 C# 语法想接触真实项目形态的开发者或者拿课程设计做二次开发底座的同学都适合拿它做起点。看懂了这套系统就摸透了传统桌面 MIS 的完整套路界面层、数据集层、业务逻辑层、数据库层怎么配合以后再写库存、进销存、订单类系统都能套用同一套思路。把数据库先跑起来再对着断点看一遍入库和出库的调用链收获比通读几章理论书更实在。2. 项目结构拆解从缓存文件、连接配置到 WMSDataSet 的三层信息2.1 从文件清单反推项目全貌每个文件是干什么的拿到一个 VS 2005 时代的 C# 项目我会先按文件清单做一次“信息体检”不急着双击打开解决方案因为很多问题在打开之前就能预判。这份源码的文件列表很有代表性无标题.bmp、ResolveAssemblyReference.cache、cangku.csproj.GenerateResource.Cache、cangku.csproj.ResolveComReference.cache、app.config、cangku.exe.config、cangku.vshost.exe.config另外还有三个强类型数据集设计文件 WMSDataSet.Designer.cs、WMSDataSet6.Designer.cs、WMSDataSet7.Designer.cs。把这些文件按用途分类一眼就能看出骨架文件归属说明无标题.bmp资源文件一般是窗体背景或启动画面检查是否被项目引用没引用直接删ResolveAssemblyReference.cache编译缓存VS 缓存程序集引用解析结果引用变动后会出“幽灵错误”cangku.csproj.GenerateResource.Cache编译缓存资源生成缓存资源文件替换后容易失真cangku.csproj.ResolveComReference.cache编译缓存COM 引用解析缓存老项目里常见app.config项目配置开发期配置源文件编译时复制为 exe.configcangku.exe.config运行配置程序运行期真正读取的配置注意它和 app.config 是两份文件cangku.vshost.exe.config宿主配置只在 VS 调试时被 vshost 进程读取手动改了不重新编译会造成“改了没生效”的假象WMSDataSet*.Designer.cs核心代码强类型数据集定义系统数据访问的枢纽自动生成但需要能看懂这里要特别强调 app.config 和 exe.config 的关系。在 .NET 2.0 时代项目里存的是 app.config编译之后生成到 bin\Debug 的是 cangku.exe.configCLR 配置系统运行时读的是后者。很多人改完 app.config 不重新生成直接按 F5发现改动没生效其实是 VS 又按原文件重新生成了一遍配置反过来有人为了省事直接改 bin 里的 exe.config一回编译就被覆盖这也算一种典型的“后悔药吃不到”现场。vshost.exe.config 这个文件是 Visual Studio 2005 引入的宿主进程机制的一部分。调试时实际跑的是 cangku.vshost.exe它优先读自己的配置。当你手动改了项目属性里的连接字符串又没触发完整重新生成界面还是老连接第一反应往往是“代码有 bug”其实就是这个隐藏配置在作怪。正式发布时这个文件不会跟随程序集一起分发不用担心。2.2 强类型数据集WMSDataSet 里到底装了什么WMSDataSet.Designer.cs 是这套系统的数据访问枢纽它来自 IDE 里的数据集设计器。在 Visual Studio 2005 中新建一个数据集文件拖入表或执行查询IDE 就会生成对应的强类型 DataSet、DataTable、DataRow 和 TableAdapter 类。所谓强类型意思是表的每一列在编译期就有对应的属性不会出现把字符串塞进数字列还等到运行时才报错的情况。你可以把 WMSDataSet 理解成“内存里的一套数据库视图”所有界面控件都通过它间接读写数据不直接碰 SqlConnection 和 SqlCommand。这个仓库系统背后的表结构按业务模块拆开大致是这样实际表名以包内 SQL 脚本为准T_Operator 操作员表 OperatorID, UserName, Password, RealName, Role T_Supplier 供货商表 SupplierID, SupplierName, Contact, Phone T_Customer 客户表 CustomerID, CustomerName, Contact, Phone T_Category 货物类别表 CategoryID, CategoryName T_Warehouse 仓库表 WarehouseID, WarehouseName, Address T_Goods 货物表 GoodsID, GoodsName, CategoryID, Spec, Unit, Price T_InStock 入库单表 InStockID, GoodsID, Quantity, InDate, OperatorID, SupplierID T_OutStock 出库单表 OutStockID, GoodsID, Quantity, OutDate, OperatorID, CustomerID T_Stock 库存表 WarehouseID, GoodsID, Quantity核心思路是“一账一流水”T_Stock 只存当前库存数字T_InStock 和 T_OutStock 存每笔业务流水。查询时可以用流水反推库存也可以直接读 T_Stock 加速展示这是进销存系统里最常见的库存设计模型。数据集同时存在多个版本的 Designer.cs 这件事第 5 章会专门讲这里先记住一个原则运行时用的是哪个表结构以当前工程文件里 Compile 节点实际包含的文件为准其余都只是设计残留。强类型数据集最方便的用法是一行代码把数据库表填进内存再绑定给界面控件// 把 T_Goods 表的数据填充到强类型数据集里 this.tbl_GoodsTableAdapter.Fill(this.wmsDataSet.Tbl_Goods); // 绑定到 DataGridView界面就显示货物列表 this.dgvGoods.DataSource this.tbl_GoodsBindingSource;这段代码的逻辑不复杂TableAdapter 负责对数据库执行查询把结果填充到 DataSet 里的对应 DataTableBindingSource 是界面和数据表之间的中间件替 DataGridView 处理定位、排序、过滤等杂事。在 VS 2005 里把数据组件从工具箱拖到窗体上设计器会自动生成这些代码但理解每一行的职责才能在出问题时定位到具体环节。2.3 app.config 与连接字符串数据库地址该放在哪一层连接字符串决定了程序去哪个数据库服务器、用哪个账号登录。在这个系统里它默认放在 app.config 的 connectionStrings 节点里connectionStrings add nameWMSConnectionString connectionStringData Source.\SQLEXPRESS;Initial CatalogWMS;Integrated SecurityTrue providerNameSystem.Data.SqlClient / /connectionStrings几个关键参数拆开看Data Source 指定服务器和实例名.\SQLEXPRESS表示本机的 SQLEXPRESS 实例Initial Catalog 指定数据库名Integrated SecurityTrue 表示用当前 Windows 账号登录。如果在你的环境里 SQL Server 用的是混合登录模式连接字符串就需要换成 User ID 和 Password 形式。数据访问层读取配置的标准写法如下using System.Configuration; using System.Data.SqlClient; // 从配置文件里取连接字符串而不是在代码里写死 string connStr ConfigurationManager.ConnectionStrings[WMSConnectionString].ConnectionString; using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); // 后续的业务操作都基于这个连接 }注意 VS 2005 的默认项目不会自动引用 System.Configuration.dll直接写 using System.Configuration 会编译报错。需要在解决方案资源管理器里右键引用找到 System.Configuration 添加上这是个很常见的编译期拦路虎代码本身没错错在引用没加。为什么不建议把连接字符串硬编码在代码里因为换一台机器、换一个数据库实例就得重新编译一次放在配置文件里只需要改一行文本。仓库管理系统要在多台机器上部署配置和代码分离是底线。老项目里最常见的改法就是改 Data Source 后的实例名第 5 章你会看到这个“实例名”能埋多大的雷。3. 环境搭建与首发运行SQL 2005 项目在当前机器上的复活方案3.1 数据库初始化备份文件、数据文件、脚本三种还原方式很多人拿到源码第一件事是打开 Visual Studio 按 F5结果界面弹出来数据全是空的或者直接报数据库连接失败。我的习惯正好相反先确认数据库能不能起来再碰界面。很多课程设计包本身没有 bug卡在数据库初始化这步会让新手误判成“源码是坏的”。这套系统用的数据库是 SQL 2005交付时通常有三种形态处理方式不一样先看包里的内容再动手形态出现标志初始化方式备份文件后缀 .bak在 SQL Server Management Studio 里右键“还原数据库”选择备份文件数据文件后缀 .mdf / .ldf右键“附加数据库”选择主数据文件脚本文件后缀 .sql用 sqlcmd 或 SSMS 打开执行按顺序建库建表如果包里给的是 .sql 脚本最常见的问题是脚本包含建库语句和建表语句两段执行顺序不能乱。用命令行工具执行最简单直接# sqlcmd 执行初始化脚本-S 指定实例-U 和 -P 是登录账号密码 sqlcmd -S .\SQLEXPRESS -U sa -P 123456 -i init_wms.sql解释一下这段命令-i 参数让 sqlcmd 读取脚本文件并逐条执行脚本里一般会有 CREATE DATABASE、CREATE TABLE、INSERT 初始数据三部分。如果脚本里没有 CREATE DATABASE 而连接字符串里又指定了 WMS 库就要先在 SSMS 里手工建一个空库再执行建表语句。这个顺序搞反了执行完会提示“数据库不存在”。3.2 连接字符串改造本机能跑和换台机器也能跑是两回事课程设计项目最常见的部署窘境是在自己机器上一切正常拷到答辩机器上就黑屏报错十有八九是连接字符串里的服务器名对不上。SQL 2005 时期开发机实例名常常是“机器名\SQLEXPRESS”这种格式拷贝到另一台机器后同样的字符串找的还是原来那台机器。正确做法是把连接字符串收敛地放到 app.config 里然后统一修改。在修改前先确定你的 SQL Server 实例名用命令查一下# 在命令行列出本机 SQL Server 服务 sc query | findstr /i MSSQL看到类似 MSSQL$SQLEXPRESS 的输出说明实例名是 SQLEXPRESS如果只有 MSSQLSERVER说明是默认实例。默认实例的连接字符串里 Data Source 直接写机器名或一个点号 . 就行add nameWMSConnectionString connectionStringData Source.\SQLEXPRESS;Initial CatalogWMS;Integrated SecurityTrue providerNameSystem.Data.SqlClient /还有一类登录问题数据库初始化时用了 sa 账号而 SQL Server 装的时候选的是“仅 Windows 身份验证模式”这时 User ID 和 Password 会被无视。需要在 SSMS 里右键服务器实例选择属性在“安全性”页把登录模式改成“SQL Server 和 Windows 身份验证模式”然后重启 SQL Server 服务。这一步不做程序连接时报错会指到密码错误其实密码根本没被使用。3.3 编译运行前的清理清单关掉缓存干扰VS 2005 项目经过多次复制、压缩、解压bin 和 obj 目录里往往残留着与源码不同步的旧编译产物。我的血泪经验是打开项目前先做一次清理能省掉后面排查诡异报错的大量时间。在 Windows 命令行进入项目根目录执行# 删除编译缓存和输出目录VS 下次打开会全部重新生成 cd /d C:\your_path\cangku del /s /q *.cache rmdir /s /q bin rmdir /s /q obj这些文件都是从源码生成的中间产物删除后 Visual Studio 打开项目会自动重建不影响源码本身反而是排除“缓存导致编译结果和代码不一致”这类玄学问题的最有效手段。ResolveAssemblyReference.cache、GenerateResource.Cache 这些文件存的是程序集引用的解析快照如果之前项目在别的机器上引用过不同路径的 DLL不删缓存就可能一直报命名空间找不到。清理完缓存后再检查两个引用项System.Configuration.dll 是否已添加System.Data.dll 是否正常。前者管配置文件读取后者管数据访问都是这个系统的基础依赖。引用就绪后按 F5 生成一次把输出窗口的内容从头到尾看一遍确认有没有警告和错误再尝试登录界面。4. 核心模块实现入库、出库、调库的数据流与事务细节4.1 入库单保存强类型数据集与 TableAdapter 的 CRUD 套路入库操作是这个系统的核心高频功能。在 WinForms 界面里用户选择货物、填写数量、选择供货商点保存后程序要把入库明细写进 T_InStock 表同时更新 T_Stock 库存表。课程设计源码里最常见的第一版写法是用强类型数据集操作简单、代码量少// 保存入库单 private void btnSaveIn_Click(object sender, EventArgs e) { // 先把当前行从编辑状态提交到 DataTable否则最后一行可能停留在编辑态不被保存 this.bdsInStock.EndEdit(); // TableAdapter.Update 会把 DataTable 中状态为 Added、Modified、Deleted 的行 // 按主键生成对应的 INSERT、UPDATE、DELETE 语句并批量执行 int affected this.tbl_InStockTableAdapter.Update(this.wmsDataSet.Tbl_InStock); if (affected 0) { MessageBox.Show(入库单保存成功); // 重新查询一次保证界面数据与数据库一致 this.tbl_InStockTableAdapter.Fill(this.wmsDataSet.Tbl_InStock); } }这段代码值得学习的地方有两处。第一务必先调用 EndEdit 把行从编辑态提交否则 DataGridView 里正在编辑的行可能丢失。第二TableAdapter 内部会根据行的 RowState 自动判断生成什么语句不用手写 SQL这是数据集模型的便利所在。但便利的代价是它只监听单一表的状态变化库存表 T_Stock 的更新就得靠另一条调用链完成这属于薄弱环节。4.2 出库与库存扣减用条件 UPDATE 把并发问题挡在 SQL 层出库操作比入库多一个关键动作扣减库存。新手最容易写出的逻辑是先查询库存数量判断够不够再更新库存。这个做法的隐患在并发两个人同时给同一个货物出库都先查到库存 10 件A 出 8 件B 出 5 件依次提交后库存可能被扣成负数后提交的人没被拦住。因为“检查”和“扣减”是两条独立的 SQL中间有时间窗。正确做法是把检查条件直接写进 UPDATE 语句让数据库在一条语句里完成判断和扣减using (SqlConnection conn new SqlConnection(connString)) { conn.Open(); SqlTransaction tx conn.BeginTransaction(); try { // 条件 UPDATE只有当库存 出库数量时才扣减 // 返回影响行数 0 表示库存不足事务回滚 string updateStock UPDATE T_Stock SET Quantity Quantity - qty WHERE WarehouseID whId AND GoodsID gid AND Quantity qty; SqlCommand cmd new SqlCommand(updateStock, conn, tx); cmd.Parameters.AddWithValue(qty, quantity); cmd.Parameters.AddWithValue(whId, warehouseId); cmd.Parameters.AddWithValue(gid, goodsId); int rows cmd.ExecuteNonQuery(); if (rows 0) { tx.Rollback(); MessageBox.Show(库存不足出库失败); return; } // 库存扣减成功后写入出库流水 string insert INSERT INTO T_OutStock (GoodsID, Quantity, OutDate, OperatorID, CustomerID) VALUES (gid, qty, GETDATE(), opId, cId); SqlCommand insertCmd new SqlCommand(insert, conn, tx); insertCmd.Parameters.AddWithValue(gid, goodsId); insertCmd.Parameters.AddWithValue(qty, quantity); insertCmd.Parameters.AddWithValue(opId, operatorId); insertCmd.Parameters.AddWithValue(cId, customerId); insertCmd.ExecuteNonQuery(); tx.Commit(); MessageBox.Show(出库成功); } catch (Exception ex) { tx.Rollback(); MessageBox.Show(出库失败 ex.Message); } }这段代码的核心在设计意图上把“判断库存是否充足”和“扣减库存”合并成一条原子 UPDATE。数据库的行锁在这一刻生效第二个出库请求会等待第一个提交后再执行这样就堵住了并发超卖的口子。T_OutStock 流水和 T_Stock 扣减被包在同一个事务里任何一步失败都整体回滚保证“流水在、库存变”永远成对出现。4.3 调库操作与档案联动主外键约束和下拉框级联调库指的是把货物从一个仓库移到另一个仓库。实现本质就是两个仓库的库存一减一增同样需要事务保护。一个容易翻车的细节是目标仓库可能还没有这个货物的库存记录直接执行 UPDATE 影响行数为 0货物就凭空消失了。所以在调库前要先判断目标仓库是否有该货物的库存行// 调库时先判断目标仓库是否有此货物的库存记录 string checkSql SELECT COUNT(*) FROM T_Stock WHERE WarehouseID toWhId AND GoodsID gid; SqlCommand checkCmd new SqlCommand(checkSql, conn, tx); int exists (int)checkCmd.ExecuteScalar(); if (exists 0) { // 没有记录则先插入一行初始库存数量为 0 string insertSql INSERT INTO T_Stock (WarehouseID, GoodsID, Quantity) VALUES (toWhId, gid, 0); // 执行插入... } // 然后做加库存的 UPDATE基础档案模块里货物类别和货物名称之间是典型的父子关系。选了类别货物下拉框就应该只显示该类别下的货物。实现上给货物下拉框设置一个 RowFilter 就行// 货物类别与货物下拉联动 private void cmbCategory_SelectedIndexChanged(object sender, EventArgs e) { DataRowView drv cmbCategory.SelectedItem as DataRowView; if (drv null) return; int categoryId Convert.ToInt32(drv[CategoryID]); DataView dv new DataView(wmsDataSet.T_Goods); dv.RowFilter string.Format(CategoryID {0}, categoryId); cmbGoods.DataSource dv; cmbGoods.DisplayMember GoodsName; cmbGoods.ValueMember GoodsID; }注意这里用as DataRowView做类型判断是因为 SelectedItem 在列表为空或数据源未绑定时可能是 null直接强转会抛异常。这一点在 C# 里经常考as 转换失败返回 null强转失败直接异常二选一取决于你要不要立刻知道错误。5. 避坑清单编译缓存、连接字符串与类型化数据集的五个翻车现场5.1 编译期翻车点缓存文件与多数据集版本现象一项目引用的 DLL 明明已经存在命名空间和类名都没写错编译却报“类型或命名空间不存在”。清理解决方案重新生成还是同样报错。原因ResolveAssemblyReference.cache 里缓存了旧引用解析快照VS 2005 在引用没变化时不会主动重新解析直接沿用缓存结果这个“黑匣子”会逼着你怀疑自己的代码。解决关闭解决方案删除项目目录下所有 .cache 文件以及 bin、obj 文件夹重新打开生成。这类缓存文件本来就是中间产物删了不影响源码重新生成后 VS 会做一次完整的引用解析。现象二项目里同时存在 WMSDataSet.Designer.cs、WMSDataSet6.Designer.cs、WMSDataSet7.Designer.cs 三个文件改代码的时候不知道该改哪一个。改完一个运行结果还是老样子或者编译直接报重复类型定义。原因这是开发过程中迭代残留。设计者在调整表结构时IDE 自动生成了新版本的数据集设计文件旧文件没有从项目中移除。如果两个文件里定义的类名相同编译器会直接报冲突如果类名不同则会存在两份互不相干的数据集定义界面引用的到底是哪个靠代码里的类型名才能确认。解决先打开这三个文件对比命名空间和类名确定当前代码里实际使用的那个类然后在解决方案资源管理器中把另外两个文件从项目中“排除”不要直接删除先备份。排除后再编译冲突立刻消失。如果某个界面引用了被排除的数据集编译会提示缺少类型照提示把对应界面的引用改过来即可。5.2 运行期与数据层翻车点连接、空值和版本兼容现象三程序在本机运行正常换一台机器启动就报“在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误”。原因连接字符串里 Data Source 写死了本机的实例名比如“MYPC\SQLEXPRESS”。换机器后它还在尝试连接那台不存在的机器。解决把 Data Source 改成相对形式.\SQLEXPRESS表示本机 SQLEXPRESS 实例点号代表 localhost或者直接改成机器名动态获取。这条内容要写进部署文档里否则每个新人接手都要踩一遍。另外顺手确认目标机器的 SQL Server 服务已启动别两边检查到最后发现是服务没开。现象四入库保存时数据是中文正常出库记录带客户信息时程序在操作 DataRow 时抛出 InvalidCastException断点定位到给文本框赋值的那一行。原因数据库里某个字段是 NULL强类型数据集生成的属性访问器遇到 NULL 时并不会自动返回空字符串它保持 DbNull 语义。直接转换成 string 会抛异常而使用 ToString() 只能得到空字符串两种情况表现不一样容易被误判成“数据被弄丢了”。解决强类型数据集为每个可空列都生成了 IsXxxNull() 判断方法访问前先做判断或者用 SQL 层的 ISNULL 函数兜底。这个坑在统计和查询模块里特别常见因为带条件查询经常出来 NULL 值。现象五Windows 10 或 11 上安装 SQL Server 2005 失败安装程序报错或装完服务起不来连带这套仓库系统没法用。原因SQL 2005 发布时间早对现代 Windows 系统的兼容性有限官方早已停止支持。课程设计里写“采用 SQL 2005”是当年的技术选型不等于必须用 SQL 2005 才能跑。解决安装新版本 SQL Server Express然后选择附加数据库或执行包内的 SQL 脚本完成建库。新版引擎对 TDS 协议向下兼容连接字符串里把版本相关的部分调整一下即可C# 代码几乎不需要改。如果包里只有 .bak 文件可以直接在新版 SSMS 里还原还原后数据库会自动升级兼容级别。这里唯一要注意的是新版还原旧备份属于单向升级别指望还能降回去动手前把原始备份复制一份留底。6. 期末盘点验证法用一条 SQL 检查库存账实是否一致前面几章解决的是“功能能不能跑”最后一章我想聊一个更实用的问题怎么验证库存数据是对的。很多人把入库、出库、调库跑通了就以为系统完工其实仓库系统真正怕的是账面上好看、实际对不上。我拿到任何进销存类项目都会先跑一遍三段式对账查询。对账思路是把流水和库存分开算再比对差值。理论上 T_Stock 的当前库存应该等于 T_InStock 入库总数减去 T_OutStock 出库总数。用子查询避免多表连接造成的行数膨胀SELECT g.GoodsID, g.GoodsName, ISNULL((SELECT SUM(Quantity) FROM T_InStock i WHERE i.GoodsID g.GoodsID), 0) AS InTotal, ISNULL((SELECT SUM(Quantity) FROM T_OutStock o WHERE o.GoodsID g.GoodsID), 0) AS OutTotal, ISNULL((SELECT SUM(Quantity) FROM T_Stock s WHERE s.GoodsID g.GoodsID), 0) AS StockTotal FROM T_Goods g;如果 StockTotal 不等于 InTotal 减 OutTotal说明数据链路里有断点。常见原因有三个一是并发场景下库存扣减和流水写入不在同一事务里导致一边成功一边失败二是初始化数据时直接插入了 T_Stock没有对应的流水记录三是调库操作只更新了库存没写流水。拿这条 SQL 查一遍哪种情况对号入座很快就能定位。这个验证方法同样可以用来考核你自己的二次开发质量。每加一个功能跑一遍对账如果破坏了账实一致说明事务边界没划清楚。水平再高一点的开发者会在数据库里加一个定时任务或存储过程做每日对账但仓库管理系统课程设计阶段手工跑这条 SQL 已经足够。你还值得做两个 C# 层面的小实验。第一个是把操作日志抽成委托让入库、出库、调库三个模块共用同一套日志记录方法省去到处复制粘贴第二个是用反射写一个通用导出工具把任意 DataGridView 的数据导出成 Excel 兼容格式属性名和列名动态映射。这两招在毕业设计答辩时都很加分因为面试官和评委最想看的就是你有没有“抽公共逻辑”的意识。我从这类旧项目里得到的教训是数据能显示出来不代表程序正确账实一致才是仓库系统的生命线。从那以后我每次拿到别人的仓库管理源码都会强制走一遍完整流程——先清理编译缓存再确认连接字符串然后跑期末对账 SQL最后才谈功能改进。这套流程救过我很多次希望也能帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?