尧图精选

C#仓库管理系统源码实战:WinForms+SQL Server从数据库附加到部署避坑

🕒 发布时间:2026/10/1 18:15:49 📁 来源:尧图网络
简介这是一套基于C#开发的仓库管理系统完整项目面向正在完成课程设计或毕业设计的计算机专业学生也适合需要快速搭建进销存原型的初级开发者。系统围绕进货、销售、库存及商品信息管理展开采用人机交互界面对录入数据进行严格校验兼顾易维护性与易操作性。压缩包共115个文件约4.34MB核心为49个C#源代码文件与数据库文件mdf/ldf同时包含界面资源、图标、可执行程序及一份详细的使用说明便于直接配置运行和学习。项目源码按功能模块组织可清晰看到窗体设计、业务逻辑与数据层的对应关系。目前已有一千四百余人下载学习。结合使用说明中的项目配置步骤读者可以快速还原系统环境在此基础上进行功能扩展或改造是理解C#桌面应用开发与数据库交互的实用参考。1. “源码数据库使用说明”这套组合大部分人的难点不在写代码而在把数据库跑起来拿到一个标题叫“基于C#的仓库管理系统源码数据库使用说明.zip”的压缩包新手最常犯的错是先去读代码读了两小时发现自己连登录页都打不开。这类C#仓库管理系统项目其实是比较标准的毕业生设计或课程设计形态WinForms窗体做界面SQL Server存数据中间用最基础的数据访问方式写增删改查。它的价值不是算法而是让你一趟走完“建表—连库—写界面—调通业务”的完整链路。难点集中在两处第一是数据库文件怎么正确附加到本地实例第二是连接字符串里的服务器名和登录方式怎么改到跟你机器一致。本文按“选型—建库—写业务—部署—避坑”的顺序把这条链路逐段拆开你可以直接拿这套方法去验收手上的源码也能照着思路自己从零搭一个。2. 技术选型与项目分层为什么这类系统多数用WinForms而不是WPF2.1 窗体框架选型WinForms在仓库管理这类内部系统里依然最省事仓库管理系统是典型的“企业内部工具型软件”核心诉求是快速落地、能操作DataGridView、能打印报表对界面美观度的要求远低于对开发效率的要求。所以压缩包里几乎见到的都是WinForms而不是WPF这不是作者保守而是WinForms在三个维度的综合成本最低DataGridView在WinForms里默认就带排序、列调整、单元格编辑绑定DataTable后几乎不用额外代码WPF的DataGrid虽然更强但需要写模板和绑定对刚起步的人来说学习曲线陡很多WinForms项目在Visual Studio里新建即用无需了解XAML、依赖属性或MVVM老教程和答案的数量级也完全不同这类系统一般只跑在局域网内、用户数量不超过几十人单机部署成本远小于维护成本。如果你拿到的源码是WPF版本也别急着换核心业务逻辑和数据库部分完全通用只是UI层的代码组织方式不同。这个项目真正值钱的部分在DAL层和SQL脚本不在窗体。2.2 数据访问层选型Dapper是把源码讲清楚的最优解常见的C#数据访问方式有三种原生的ADO.NETSqlConnection SqlCommand、轻量ORM框架Dapper、重量级ORM框架Entity Framework。这个仓库管理系统源码只要不是特别老多数会用Dapper因为它既能让你看清SQL语句本身又省掉了ADO.NET里反复写DataReader取值赋值的样板代码。用NuGet安装Dapper是第一步在“解决方案资源管理器”里右键项目选“管理NuGet程序包”搜索Dapper安装最新稳定版即可。// 第一步在App.config里配置好连接字符串 // connectionStrings // add nameWMSDB connectionStringServer.;DatabaseWarehouseMS;User Idsa;Password123456;TrustServerCertificateTrue providerNameSystem.Data.SqlClient / // /connectionStrings // 第二步写一个通用的数据库访问类后面的所有模块都复用它 using Dapper; using System.Configuration; using System.Data.SqlClient; public class DbHelper { private static readonly string connStr ConfigurationManager.ConnectionStrings[WMSDB].ConnectionString; // 查询列表 public static ListT QueryT(string sql, object param null) { using (var conn new SqlConnection(connStr)) { return conn.QueryT(sql, param).ToList(); } } // 执行增删改返回受影响行数 public static int Execute(string sql, object param null) { using (var conn new SqlConnection(connStr)) { return conn.Execute(sql, param); } } // 查询单行 public static T QueryFirstT(string sql, object param null) { using (var conn new SqlConnection(connStr)) { return conn.QueryFirstOrDefaultT(sql, param); } } }这段代码的逻辑在于每次操作都创建一个新的SqlConnection用完马上释放避免连接泄漏。using语句保证即使SQL执行抛异常连接也会被关闭。Dapper的Query和Execute是扩展方法接收一个SQL字符串和参数对象。参数对象用匿名类型new { }传入例如new { Id 1 }Dapper会自动映射成SQL参数防止SQL注入。注意连接字符串里的TrustServerCertificateTrue这是针对SQL Server 2019之后默认强制加密连接设置的不加这一项本机连接也可能报“证书链是由不受信任的颁发机构颁发的”错误。2.3 三层架构的目录划分拿到源码后先按这个结构定位文件大多数这类源码会把项目分成Models、DAL、BLL、UI四个目录少数还会加一个Common放公共方法。如果你解压后看到的文件夹结构不是这样也别慌先找“.sln”后缀的解决方案文件用Visual Studio打开后看项目内部结构。常见的四层职责如下Models层存放实体类比如Product、Stock、InboundOrder类的属性跟数据库表的字段一一对应DAL层数据访问层存放所有SQL操作是代码里最值得花时间读的部分因为这里能看到每张表的SQL语句和业务约束BLL层业务逻辑层对DAL进行组合比如入库操作要同时调用“新增入库单”和“更新库存”两个DAL方法组合动作放在BLL里做为了保证原子性会在这一层加事务UI层WinForms窗体只负责把用户输入传给BLL、把结果显示到DataGridView不直接写SQL。打开源码后建议先不要看Form1.cs的界面代码而是从DAL文件夹开始读。你只需要回答三个问题就能判断这个项目能不能用每个业务表有没有对应的Insert/Update/Delete方法、库存变更是不是用事务包裹、登录密码有没有做哈希处理。这三个问题直接决定这套源码是能跑的教学玩具还是能用的管理系统。3. 数据库设计把仓库管理的核心表一次建对3.1 建表脚本八张表构成一个可扩展的库存模型拿到“源码数据库”压缩包时数据库通常有两种承载形式一是附带一个.bak备份文件或.mdf数据库文件让你直接附加二是附带一个.sql脚本让你执行建库建表。无论是哪种你都应该先读懂表结构再动代码。以最常见的SQL Server实现为例主表至少有这八张用户表Users、供应商表Supplier、客户表Customer、商品表Product、入库单表InboundOrder、入库明细表InboundOrderDetail、出库单表OutboundOrder、出库明细表OutboundOrderDetail。-- 建库如果脚本里没有这一段先手动执行 IF DB_ID(WarehouseMS) IS NULL CREATE DATABASE WarehouseMS; GO USE WarehouseMS; GO -- 商品表 CREATE TABLE Product ( ProductId INT IDENTITY(1,1) PRIMARY KEY, ProductCode NVARCHAR(50) NOT NULL UNIQUE, -- 商品编码条码扫描时用 ProductName NVARCHAR(200) NOT NULL, -- 商品名称 Spec NVARCHAR(100) NULL, -- 规格型号 Unit NVARCHAR(20) NOT NULL DEFAULT N件, -- 计量单位 CategoryId INT NULL, -- 分类ID可外键到Category表 IsDelete BIT NOT NULL DEFAULT 0 -- 软删除标记避免误删历史单据 ); GO -- 库存表一个商品只有一条记录错仓库管理里必须按仓库维度存 CREATE TABLE Stock ( StockId INT IDENTITY(1,1) PRIMARY KEY, ProductId INT NOT NULL REFERENCES Product(ProductId), WarehouseId INT NOT NULL REFERENCES Warehouse(WarehouseId), Quantity DECIMAL(18,3) NOT NULL DEFAULT 0, -- 用decimal不要用float精度坑在后文 SafetyStock DECIMAL(18,3) NOT NULL DEFAULT 0 -- 安全库存低于这个值需要补货 ); GO -- 入库单主表 CREATE TABLE InboundOrder ( InboundId INT IDENTITY(1,1) PRIMARY KEY, OrderNo NVARCHAR(32) NOT NULL UNIQUE, -- 单号格式例如 RK202501010001 SupplierId INT NULL REFERENCES Supplier(SupplierId), OperatorId INT NOT NULL REFERENCES Users(UserId), -- 操作员 InboundDate DATETIME NOT NULL DEFAULT GETDATE(), Remark NVARCHAR(500) NULL, Status TINYINT NOT NULL DEFAULT 0 -- 0草稿1已过账9已作废 ); GO -- 入库明细表 CREATE TABLE InboundOrderDetail ( DetailId INT IDENTITY(1,1) PRIMARY KEY, InboundId INT NOT NULL REFERENCES InboundOrder(InboundId), ProductId INT NOT NULL REFERENCES Product(ProductId), Quantity DECIMAL(18,3) NOT NULL, Price DECIMAL(18,2) NULL, -- 进货单价 Amount AS (Quantity * Price) PERSISTED -- 计算列自动求金额 );这段DDL的重点有几个。第一个是Product表里加IsDelete软删除位而不是物理删除因为商品一旦被历史单据引用物理删除会导致明细表外键悬空。第二个是Stock库存表同时带ProductId和WarehouseId库存模型必须是“商品×仓库”的维度单仓系统压缩掉了Warehouse维度但真正的仓库管理系统如果按单仓设计后期加仓库时所有表都要改。第三个是DECIMAL(18,3)不取整仓库里按吨、按米计算的物料一定会有小数float会有精度漂移。Amount列用PERSISTED计算列避免在C#代码里做金额累加累加放数据库更可靠。如果你在附加数据库后看不到这些表用SQL Server Management Studio连接上实例展开“数据库”→“WarehouseMS”→“表”就能看到。如果只有Product没有Stock说明源码可能把库存数量直接挂在Product表里了这种设计在校验时会出问题后面第5章里会讲。3.2 外键、索引和约束源码能否在重压下不出错的关键很多C#课程设计源码建表时不加外键理由是“方便手动删数据”但是仓库管理系统的核心是数据一致性删了外键很快就会产生孤儿数据。建议在实际项目里至少保留这几个约束明细表通过外键关联主表并加ON DELETE CASCADE删除入库单时自动清空明细Stock表对(ProductId, WarehouseId)加唯一约束防止同商品同仓库出现两条记录每个表的创建时间和操作人字段不要省略盘点时要按时间维度倒查。此外应该给高频查询字段补上索引。最容易踩的坑是只在主键上建索引但实际查询条件写的是OrderNo或ProductCode。一进货就卡顿的单仓系统十有八九是把商品编码列当普通列在InboundOrderDetail里扫。下面是这类系统应该有的索引结构。CREATE NONCLUSTERED INDEX IX_InboundDetail_ProductId ON InboundOrderDetail(ProductId); CREATE NONCLUSTERED INDEX IX_OutboundDetail_ProductId ON OutboundOrderDetail(ProductId); CREATE NONCLUSTERED INDEX IX_InboundOrder_OrderNo ON InboundOrder(OrderNo);这些索引针对的核心场景按顺序是出库单按商品聚合统计库存流水、入库时按单号查重、报表里按日期区间关联明细表。如果压缩包里的脚本没写这段你就手动补上再跑一次对查询性能的提升是数量级的。3.3 初始数据为什么登录不进去八成是数据脚本没执行源码附带的数据库文件如果是从作者机器上直接拷出来的.mdf登录账号密码就是作者设置的。最常见的情况是附加成功后打开登录窗体输入代码里写死的“admin/123456”结果报“登录失败”。原因通常有两个一个是Users表里的密码列存的是哈希值代码里存的不是明文另一个是登录时用了SELECT * FROM Users WHERE UserNameadmin AND Passwordadmin但数据库里根本没有这一行。建议在SQL Server Management Studio里选中Users表查看前20行确认一下种子数据是否导入成功。如果表是空的手动补入一条管理员记录密码写成代码里对应算法的密文。如果源码用的是MD5SQL里这样生成INSERT INTO Users (UserName, PasswordHash, RealName, IsAdmin) VALUES (Nadmin, CONVERT(NVARCHAR(32), HASHBYTES(MD5, 123456), 2), N系统管理员, 1);注意HASHBYTES返回的是varbinary必须用CONVERT(..., 2)转成十六进制字符串否则C#端比较时会发现格式对不上。很多源码在这方面实现不一致有的在C#里算MD5有的在SQL里算一定要先验证算法的一致性。4. 业务模块实现登录、入库、出库、盘点怎么落代码4.1 登录模块密码不能明文存储参数化查询是底线读这套源码最先看的模块就是登录。如果登录代码里是字符串拼接SQL后面所有业务代码的写法都不会太规范。正确的登录流程是前端把用户名密码传给BLL层BLL调用DAL查用户DAL用哈希值比对。public class UserService { // 登录方法 public User Login(string userName, string password) { // 把用户输入的密码做MD5哈希 string pwdHash Md5Helper.Compute(password); // 用参数化查询避免SQL注入 string sql SELECT UserId, UserName, RealName, IsAdmin FROM Users WHERE UserName UserName AND PasswordHash PwdHash AND IsDelete 0; return DbHelper.QueryFirstUser(sql, new { UserName userName, PwdHash pwdHash }); } }参数说明这里的UserName和PwdHash对应匿名对象的属性名大小写不敏感但必须拼写一致。MD5Helper是公共类内部实现用System.Security.Cryptography.MD5。不要在SQL里写WHERE UserName userName 这行代码在任何数据库上都是注入漏洞。另外登录后不要只存一个用户名到全局变量建议把User对象整个放进静态类里方便后续所有窗体读取当前操作员ID因为入库单和出库单都要记录操作人。4.2 入库过账用事务把主表、明细表和库存串起来仓库管理系统最容易出错的地方就是入库操作用户填一张入库单包含多行商品点击“过账”后系统要做三件事——写入入库单主表、写入入库明细表、增加对应库存。任何一步失败都不能留半截数据。源码里如果这三步没用事务包裹就基本上是教学演示级别的了。真正落地的写法如下using (var conn new SqlConnection(connStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { try { // 1. 生成单号并写入入库单主表 string orderNo GenerateOrderNo(RK); // 例如RK202501010001 string sqlInbound INSERT INTO InboundOrder(OrderNo, SupplierId, OperatorId, InboundDate, Status) VALUES (OrderNo, SupplierId, OperatorId, GETDATE(), 1); SELECT CAST(SCOPE_IDENTITY() AS INT);; int inboundId conn.QuerySingleint(sqlInbound, new { OrderNo orderNo, SupplierId dto.SupplierId, OperatorId currentUser.UserId }, tran); // 2. 明细表逐行插入 string sqlDetail INSERT INTO InboundOrderDetail(InboundId, ProductId, Quantity, Price) VALUES (InboundId, ProductId, Quantity, Price);; foreach (var item in dto.Items) { conn.Execute(sqlDetail, new { InboundId inboundId, ProductId item.ProductId, Quantity item.Quantity, Price item.Price }, tran); // 3. 同步更新库存表注意这里的存量判断 string sqlStock IF EXISTS(SELECT 1 FROM Stock WHERE ProductIdProductId AND WarehouseIdWarehouseId) UPDATE Stock SET Quantity Quantity Quantity WHERE ProductIdProductId AND WarehouseIdWarehouseId; ELSE INSERT INTO Stock(ProductId, WarehouseId, Quantity, SafetyStock) VALUES(ProductId, WarehouseId, Quantity, 0);; conn.Execute(sqlStock, new { ProductId item.ProductId, WarehouseId dto.WarehouseId, Quantity item.Quantity }, tran); } tran.Commit(); } catch { tran.Rollback(); throw; } } }这段代码的逻辑说明BeginTransaction获取一个事务对象事务对象必须作为参数传给Dapper的每个Execute和Query方法这是保证它们参与同一事务的关键。先查主表自增ID再循环插入明细每次同时更新库存。这里库存更新用了IF EXISTS判断是为了兼容“第一次入库时库存表还没有该商品记录”的情况。使用UPSERT语法IF ELSE比先查后插少一次往返在单表单仓场景下性能完全足够。如果源码里没有WarehouseId即单仓模型就把这个字段从SQL中剔除但库存判断逻辑不变。事务范围要覆盖“写单更新库存”不能只包裹明细插入。4.3 出库与库存扣减避免负库存的三个方案出库业务和入库高度对称唯一需要额外处理的是负库存问题。按什么扣减库存三种常用策略是出库时扣当前可用库存、禁用负库存事务里查库存数不足则抛异常、允许负库存但做预警。仓库管理系统源码多数采用第二种实现上是在同一事务里先查库存再扣减。// 扣减库存前必须做的检查 string checkSql SELECT Quantity FROM Stock WITH (UPDLOCK, HOLDLOCK) WHERE ProductId ProductId AND WarehouseId WarehouseId; decimal qty conn.QueryFirstOrDefaultdecimal(checkSql, new { ProductId item.ProductId, WarehouseId dto.WarehouseId }, tran); if (qty item.Quantity) { throw new Exception($商品ID {item.ProductId} 库存不足当前库存 {qty}); } string updateSql UPDATE Stock SET Quantity Quantity - Quantity WHERE ProductId ProductId AND WarehouseId WarehouseId; conn.Execute(updateSql, new { ProductId item.ProductId, WarehouseId dto.WarehouseId, Quantity item.Quantity }, tran);这段代码里加了WITH (UPDLOCK, HOLDLOCK)表提示这是关键。如果只是SELECT再UPDATE两个并发用户同时出库同一商品时可能都读到库存5一个扣3一个扣4最终库存变成-2。UPDLOCK让查询在事务期间锁住这条库存记录第二个事务必须等第一个提交后才能读取。HOLDLOCK防止其他事务在这个事务结束前插入新的同商品记录。这种写法在源码里很少见但你需要知道它是保证数据正确的核心。盘点模块一般是在主界面放一个“生成盘点单”按钮遍历Stock表生成盘点明初用户录入实盘数系统计算盈亏数量并生成损益记录。这一模块的源码结构最固定不外乎一张盘点主表和一张明细表核心SQL是UPDATE Stock JOIN盘点明细。5. 把压缩包跑起来数据库附加、连接串修改与常见问题避坑5.1 第一步不是打开Visual Studio而是把数据库先弄上线接到这类压缩包规范的操作顺序是先看数据库文件再开代码。解压后找扩展名为.mdf、.ldf或.bak的文件。如果拿的是.mdf用SQL Server Management Studio执行附加如果是.bak则用还原。-- .mdf附加方式路径换成你机器上的实际位置 CREATE DATABASE WarehouseMS ON (FILENAME ND:\WMS\WarehouseMS.mdf), (FILENAME ND:\WMS\WarehouseMS_log.ldf) FOR ATTACH; GO附加失败最常见的报错是“无法打开物理文件拒绝访问”原因不是文件损坏而是SQL Server服务账号没有权限读取这个目录。解决方法是右键数据库文件在“属性→安全→编辑”里给“MSSQLSERVER”或SQL Server服务账号一般是NT Service\MSSQLSERVER取决于实例名添加完全控制权限。还有一种更快的做法把.mdf和.ldf复制到C:\Program Files\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQL\DATA目录下再附加该目录默认已授权。5.2 连接字符串的四个必改项服务器名、登录方式、库名、证书打开代码里的App.config文件找到connectionStrings节点把下面这几个值按你的环境改掉connectionStrings add nameWMSDB connectionStringServer.;DatabaseWarehouseMS;User Idsa;Password你的密码;TrustServerCertificateTrue providerNameSystem.Data.SqlClient / /connectionStrings第一项Server.表示本机默认实例如果你装的是命名实例比如MSSQLSERVER2019就得写成Serverlocalhost\MSSQLSERVER2019。第二项User Idsa是SQL Server登录账号如果安装时选了“Windows身份验证模式”则要改用Trusted_ConnectionTrue并去掉用户名密码。第三项Database一定要和附加的数据库名一致。第四项TrustServerCertificateTrue是针对SQL Server 2019及以上版本的强制加密通信设置不加会报“证书链是由不受信任的颁发机构颁发的”。遇到“无法连接到数据库”时不要先怀疑代码先用SSMS用同一组账号密码手动连一次如果SSMS连不上代码一定连不上。5.3 五个高频翻车场景现象、原因、解决场景一附加数据库成功但程序运行报“找不到数据库WarehouseMS”。原因多半是程序里连的是另一个实例或者连接字符串里Database名和实际附加的库名不一致。解决在SSMS里执行SELECT DB_ID(WarehouseMS)检查返回NULL说明库里没有再核对App.config里的Database字段是否带了多余空格。场景二登录窗口输入admin/123456一直提示“用户名或密码错误”。原因最可能是Users表为空或密码哈希不一致。解决打开SSMS执行SELECT * FROM Users查查有没有数据若有数据把密码改成与代码相同的哈希方式。先看代码里HashHelper用的什么算法MD5、SHA1或SHA256再对照SQL插入语句。场景三运行时报“类型初始值设定项引发异常”或“在System.Data.SqlClient中未找到Dapper”。原因是NuGet包对应程序集没被正确复制到输出目录。解决在Visual Studio里右键项目选“管理NuGet程序包”确认Dapper和System.Data.SqlClient已安装再检查App.config里的providers配置是否缺失。场景四程序启动后中文全部变成乱码“???”。原因多数是连接字符串里没加CharacterSet或数据库排序规则是SQL_Latin1_General_CP1_CI_AS而非Chinese_PRC_CI_AS。解决SQL Server中把数据库排序规则改为Chinese_PRC_CI_ASALTER DATABASE WarehouseMS COLLATE Chinese_PRC_CI_AS。注意这条语句必须在无连接占用时执行。场景五盘点或报表查询时数据对不上比如金额多了0.1。原因是用了float或double存金额。解决检查数据库列类型是否是decimal如果已经存了float类型数据只能写UPDATE脚本迁移到decimal这一步没有后悔药所以建表时就要坚持用decimal。5.4 做一个“备份与恢复”脚本防止自己把开发库玩崩仓库管理系统的开发期最容易误删数据。一个简单的做法是把数据库备份脚本写成.bat每天收工前双击执行一次。echo off set Today%date:~0,4%%date:~5,2%%date:~8,2% sqlcmd -S . -U sa -P 你的密码 -Q BACKUP DATABASE [WarehouseMS] TO DISKD:\WMS\Backup\WarehouseMS_%Today%.bak WITH INIT echo 备份完成: D:\WMS\Backup\WarehouseMS_%Today%.bak pause这段批处理里%date%的格式依赖Windows区域设置如果你的机器日期格式不是yyyy-MM-dd截取的字符串位置会错位备份文件名会变成乱码。更稳妥的做法是在SQL Server里新建维护计划或作业用系统自带的时间函数生成文件名DECLARE FileName NVARCHAR(200) ND:\WMS\Backup\WarehouseMS_ CONVERT(NVARCHAR(8), GETDATE(), 112) N.bak。这个格式永远是YYYYMMDD不受区域设置影响。6. 从“能跑”改成“能用”批量导入导出、异步查询和权限细化验收代码之后如果想把这个系统真正交给仓管员用至少要做三个升级。第一个是Excel批量导入商品和入库单。仓管手里几乎必然有Excel台账手工在DataGridView里一行行录入几百条商品的体验非常糟。导入方案是用NPOI或Microsoft.Office.Interop.Excel读取表头逐行校验必填字段后批量写入数据库单批次控制在500行以内超过就分批提交并用事务包裹这样中途出错时回滚范围可控。第二个升级是把UI线程里的同步数据库操作改成异步。WinForms里查询大表时界面会卡死几秒仓管会以为程序崩溃。用了async/await之后调用DAL层时返回TaskList UI界面用await接收结果数据加载期间窗体依然能拖动。注意Dapper本身不支持异步命令但不妨碍你用Task.Run包装同步调用在单机并发量不高的系统里这是一种务实做法。第三个升级是权限细化。仓库里入库员、出库员、库存管理员、系统管理员四类角色的操作范围完全不同。最简单的方式是给Users表加RoleId字段每个窗体的Load事件里判断当前用户的角色用按钮的Visible属性控制可见性。这个改造量最小但非常见效。我自己的习惯是拿到这套源码先把SQL脚本通读一遍再看DAL层。走了这么多年弯路最深的教训是连接字符串和数据库附加永远比业务代码更先坑人不要一上来就改Form代码。每次部署到新机器时按照“附加数据库→改连接串→验证登录→测一条入库单→查库存流水”这个顺序走一遍基本上半小时就能判断一套源码值不值得留下。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →