ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#超市管理系统源码解读:从项目结构到核心业务二开实战

C#超市管理系统源码解读:从项目结构到核心业务二开实战 简介这是一套基于C#语言开发的超市管理系统源码面向C#初学者、软件工程学习者以及需要完成课程设计或毕业设计的学生致力于解决超市商品信息管理、库存变化跟踪、销售数据记录等日常业务的信息化落地问题。压缩包共13个文件以6个.cs源码文件为主干配合解决方案文件.sln、工程文件.csproj、界面资源.resx与应用配置App.config包体仅14KB结构精炼适合快速把握一个Windows窗体项目的组织方式。目前已有693人学习浏览实践参考热度不错。通过研读这套源码可以直观理解窗体界面与事件处理、数据库连接配置、基础增删改查操作、业务逻辑封装及异常处理等关键编码思路尤其适合作为从零搭建小型管理信息系统的起步模板。1. 拿到C#超市管理系统源码.zip第一件事不是解压很多教程会从“新建一个Windows窗体项目”开始但你已经拿到了需要二开的超市管理系统源码项目是现成的反而不知道先点哪里。卡壳通常发生在同一个位置VS一按F5先是数据库连接失败又是依赖项找不到最后怀疑源码缺文件。实际上超市管理系统属于C#里最典型的业务系统样本商品要建档、进货要加库存、收银要减库存还要写销售流水三个动作环环相扣源码比单纯的C#入门示例长得多也更能练到东西。这篇文章面向两类人一类是学过C#基础、想啃一个完整系统的开发者另一类是拿这套源码做课程设计或毕业设计的学生。目标只有一个让你到手之后能看懂项目结构改得动核心逻辑最后把系统跑起来。2. 先别急着跑读懂超市管理系统的项目分层与启动流程2.1 从源码目录判断是三层架构还是单窗体硬写解压C#超市管理系统源码.zip之后第一步不是找.sln而是先看有没有Models、DAL、BLL、UI这样的目录。我的习惯是先打开解决方案资源管理器花五分钟把这些项目名过一遍带DAL的是数据访问层带BLL的是业务逻辑层带UI或Views的是窗体项目。如果整个解决方案只有一个项目下面直接堆着Form1.cs、Form2.cs那就是单体写法业务和界面混在一起读起来费劲但好在改动入口直观。判断标准很简单打开任意一个窗体文件的代码看看按钮点击事件里有没有直接写SqlConnection和SqlCommand。有就需要边读边做“拆层”的心理准备没有说明作者至少用了SqlHelper封装底子还行。拿到的源码如果带SqlHelper.cs相当于已经把高层建筑的地基打好了后面所有SQL都要经它的手这也是为什么我建议先通读完这个文件再跑程序。目录/文件典型职责常见隐患SqlHelper.cs封装SqlConnection与命令执行没有参数化到处拼接SQLModels/实体类与商品、库存、订单表对应的类字段类型与表结构不一致BLL进货、销售等业务规则与DAL直接混写DAL数据访问与SQL语句硬编码表名、库名UI/窗体WinForms界面与事件事件里写大量业务逻辑看目录时还注意一点有没有Reports、Print、Common这样的子目录。超市管理系统大多要做小票打印和报表导出有独立目录说明作者把公共方法抽出来了二开时改一处就行。这类细节比记住代码本身更有价值因为它直接决定你后面动手时改哪个文件。2.2 WinForms入口与登录跳转理解从Main到主窗体WinForms程序不按你“看到”的第一个窗体的执行顺序来它的入口固定是Program.cs里的Main方法。很多初学者把登录窗体和主窗体之间的跳转写在Form_Load里结果登录后主窗体一闪而过就是因为没弄明白Application.Run的语义。static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); // 先显示登录窗体只有确认通过才进入主窗体 using (frmLogin login new frmLogin()) { DialogResult result login.ShowDialog(); if (result DialogResult.OK) { Application.Run(new frmMain()); } else { // 用户直接点了关闭不能继续后面的流程 Application.Exit(); } } }这段代码的逻辑说明ShowDialog以模态方式显示登录窗体所以执行会停在这里等待用户操作登录窗体的“确定”按钮里通常是先校验用户名和密码再把这个窗口的DialogResult设置为OKMain里拿到OK后才去创建主窗体。如果改成直接把Application.Run(new frmMain())放在Main里登录逻辑就无法真正拦截入口。参数上Application.EnableVisualStyles()对应的是现代一点的控件外观建议保留不然后面换肤或改样式容易出兼容问题。2.3 改连接字符串前先确认本机SQL环境超市管理系统的源码大概率基于SQL Server打开App.config或Web.config就能看到connectionStrings节点。常见的默写法是Server.;DatabaseSuperMarketDB;User IDsa;Password123456里面有三处必须按本机环境改第一是Server.代表本机默认实例如果你装的是命名实例要写成本机名加实例名第二是登录方式源码大概率给了SQL Server身份验证如果本机只开启了Windows验证就要先改数据库的认证模式第三是数据库名称不一定叫SuperMarketDB需要与建库脚本对应。connectionStrings add nameSuperMarketDb connectionStringServer.;DatabaseSuperMarketDB;User IDsa;Password123456;EncryptFalse;TrustServerCertificateTrue providerNameSystem.Data.SqlClient / /connectionStrings提示拿到源码后先别急着按F5先打开数据库管理工具把项目里的.sql脚本一般在Database或SQL文件夹里按顺序执行一遍。顺序反了会导致外键关联的建表失败这是源码包最常见的启动障碍。执行完脚本再回到VS里按CtrlF5编译错误会少一大半。3. 数据库设计决定改动成本商品、库存、销售流水怎么建3.1 四张核心表的主外键关系C#超市管理系统源码里的表可能很多但核心只有四张商品信息表、库存表、销售主表、销售明细表。次要的包括进货单、用户表、供应商表、操作日志表。拿到源码先画一遍关系商品表通过ProductId一对一关联库存表销售主表通过OrderNo一对多关联销售明细销售明细再通过ProductId反查商品表。这样画完后面改代码时你一眼就能看出“扣库存要动几张表”。表名主键外键与其他表关系ProductProductId无被Inventory、SaleOrderDetail引用InventoryProductIdProductId与Product一对一SaleOrderOrderNo无与SaleOrderDetail一对多SaleOrderDetailDetailIdOrderNo, ProductId关联SaleOrder和Product画关系图的时候要额外注意一件事表名以SYS开头还是以T开头不重要重要的是字段命名是否统一。有的源码把商品编码写成Id有的写成ProductCode有的表叫Goods而另一张表叫Product这种情况通常说明这套源码被人改过不止一版二开前先把字段术语统一掉能省很多排错时间。3.2 商品表和库存表为什么分开逻辑删除的应用场景从C#面向对象的角度一个商品对象应同时拥有“静态属性”和“动态属性”。名称、单位、售价是静态的相对不常变化库存数量是动态的每次进货和收银都会变。如果全都塞进一张表那么商品恢复、调价、出入库日志的记录都会让表结构变得臃肿。源码里常见的正确做法是两张表Product只管商品档案Inventory只管数量。用SQL建两张表的脚本如下。CREATE TABLE Product ( ProductId INT IDENTITY(1000,1) PRIMARY KEY, BarCode NVARCHAR(32) NOT NULL UNIQUE, ProductName NVARCHAR(64) NOT NULL, Unit NVARCHAR(8) NULL, SalePrice DECIMAL(10,2) NOT NULL DEFAULT 0 ); CREATE TABLE Inventory ( ProductId INT PRIMARY KEY, Quantity INT NOT NULL DEFAULT 0, LastUpdated DATETIME DEFAULT GETDATE(), CONSTRAINT FK_Inventory_Product FOREIGN KEY (ProductId) REFERENCES Product(ProductId) );这个建表脚本有两个细节值得记住第一IDENTITY(1000,1)把初始编号设成1000是为了跟1000以下的备用编码区分开实际超市里条码扫描靠的是BarCode字段ProductId只是内部关联标识第二Inventory的ProductId既是主键又是外键通过外键约束保证一条商品记录最多只能出现在库存表里一行多行会对账出错。源码里如果出现“商品多了几条重复库存”的Bug多半是建表时没加这个主键约束。3.3 销售主表和明细表一次收银为什么占两条数据超市收银一次可以买很多件商品如果用一张表存“这一次交易”和“这一次交易里的每种商品”就会出现同一订单号重复多次的情况查询汇总时很容易漏数或重复统计。所以常规设计是销售主表存一次交易的汇总信息销售明细表存每一行的商品、数量和价格。两张表靠OrderNo关联。CREATE TABLE SaleOrder ( OrderNo NVARCHAR(20) PRIMARY KEY, TotalAmount DECIMAL(10,2) NOT NULL, CreatedAt DATETIME DEFAULT GETDATE() ); CREATE TABLE SaleOrderDetail ( DetailId INT IDENTITY PRIMARY KEY, OrderNo NVARCHAR(20) NOT NULL, ProductId INT NOT NULL, Quantity INT NOT NULL, Price DECIMAL(10,2) NOT NULL, CONSTRAINT FK_SaleDetail_Order FOREIGN KEY (OrderNo) REFERENCES SaleOrder(OrderNo), CONSTRAINT FK_SaleDetail_Product FOREIGN KEY (ProductId) REFERENCES Product(ProductId) );注意这里的主表外键只建在了明细表上主表没有反向外键因为主外键关系只需要从“多”的一端指向“一”的一端。OrderNo用NVARCHAR而不用INT是为了支持“日期序号”的自定义单号规则比如20250312345。源码里如果单号用自增字段做主键会出现两个问题一是删除记录后单号不再连续财务对账要解释半天二是多台收银机同时开单时自增冲突。所以拿到源码先看主表主键是字符串单号就放心是自增整型就要考虑改成Guid或程序生成单号的方案。3.4 想判断源码是否可靠先查库存扣减有没有负数保护这里比较关键。很多源码能把界面跑出来但根本不敢放生产原因只有一个库存扣减没有约束。销售模块里常见写法是UPDATE Inventory SET Quantity Quantity - qty WHERE ProductId pid如果销售数量超过已有库存Quantity就会变成负数。要检查源码的SQL里有没有类似“库存不足”的判断最稳妥的方法是用一条检查性SQL扫描现有数据看有没有存量异常为负的记录。SELECT p.ProductName, i.Quantity FROM Inventory i JOIN Product p ON i.ProductId p.ProductId WHERE i.Quantity 0;把这条SQL放进数据库查询窗口跑一遍如果查询结果有数据说明源码在扣库存前没有做库存校验没有数据也只能说明当前数据是好的不能保证下一次收银不出现负数。要彻底解决需要在C#业务层加一层判断先从库存表查出Quantity数量不足时直接终止收银。这也是C#超市管理系统源码二开中最常见的功能增强点。4. 核心业务模块的源码改法商品管理、进货入库、收银台4.1 商品管理用DataGridView绑定而不是一行行Add商品管理窗体是所有超市系统源码里第一个应该改的模块。很多教材级写法是dgv.Rows.Add()手动填行性能差翻页还要自己维护。我更推荐的做法是先把查询结果装进DataTable再赋给DataGridView的DataSource配合AutoGenerateColumns自动生成列改动量小适合直接在源码上替换。private void LoadProductList(string keyword) { string sql SELECT ProductId, BarCode, ProductName, Unit, SalePrice FROM Product WHERE ProductName LIKE keyword OR BarCode LIKE keyword ORDER BY ProductId DESC; using (SqlConnection conn new SqlConnection(ConnectionString)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(keyword, % keyword %); conn.Open(); SqlDataAdapter da new SqlDataAdapter(cmd); DataTable dt new DataTable(); da.Fill(dt); dgvProduct.DataSource dt; dgvProduct.Columns[ProductId].Visible false; // 主键不显示 dgvProduct.Columns[BarCode].HeaderText 条码; dgvProduct.Columns[ProductName].HeaderText 商品名称; dgvProduct.Columns[Unit].HeaderText 单位; dgvProduct.Columns[SalePrice].HeaderText 售价; } }逻辑说明代码把LIKE查询的关键词用参数化方式放进SQL避免直接用字符串拼接Bind之后手动设置列头文字这样界面显示中文实体字段仍保留英文命名。参数上需要注意%号要写在参数值里不要写在SQL模板里否则使用索引时容易失效。DataGridView绑定模式最大的好处是新增、修改后只要重新调用这个方法界面数据就会整体刷新不用再逐个清空行这也符合后续做分页查询时的思路。4.2 进货入库SqlTransaction把两个SQL操作绑成一件完整的事进货模块的逻辑比商品管理难在它要同时做两件事写一张进货单然后更新库存。写成两条独立的SqlCommand就会出现“进货单写了但库存没加”的中间状态。源码里如果两条SQL顺序执行却没用事务这个必须改。常见做法是用SqlTransaction把插入进货单和更新库存包在同一个事务里。public void ImportStock(int productId, int quantity, decimal costPrice) { using (SqlConnection conn new SqlConnection(ConnectionString)) { conn.Open(); SqlTransaction tran conn.BeginTransaction(); try { // 1. 写入进货流水 string insertSql INSERT INTO PurchaseTotal(ProductId, Quantity, CostPrice, CreatedAt) VALUES(productId, quantity, costPrice, GETDATE()); using (SqlCommand cmd new SqlCommand(insertSql, conn, tran)) { cmd.Parameters.AddWithValue(productId, productId); cmd.Parameters.AddWithValue(quantity, quantity); cmd.Parameters.AddWithValue(costPrice, costPrice); cmd.ExecuteNonQuery(); } // 2. 更新库存表数量加进货数 string updateSql UPDATE Inventory SET Quantity Quantity quantity, LastUpdated GETDATE() WHERE ProductId productId; using (SqlCommand cmd new SqlCommand(updateSql, conn, tran)) { cmd.Parameters.AddWithValue(productId, productId); cmd.Parameters.AddWithValue(quantity, quantity); cmd.ExecuteNonQuery(); } tran.Commit(); } catch { tran.Rollback(); throw; // 把异常抛给上层窗体显示不在数据层吞掉 } } }这段代码的两个动作实际上指向同一个业务目标所以不能以“数据库自己会回滚吗”来赌必须显式Commit或Rollback。BeginTransaction之后所有SqlCommand必须绑定tran参数一个漏掉都会在提交时报“事务未关联”的错误。另外这里先说写流水再更新库存顺序反了也不会出错但习惯上先写流水因为流水是业务的事实来源库存只是由它推导出来的结果出错时更容易对账。写法风险适用场景两条SqlCommand独立执行库存和流水不一致只做Demo演示SqlTransaction事务包裹要么全部成功要么全部回滚正常生产要求4.3 收银台多行明细、找零与库存扣减的前后台顺序收银台源码通常由一个“添加商品”按钮、一个显示当前售出商品的DataGridView、一个合计金额文本框、一个“结账”按钮组成。新手改这块时容易把“界面显示”和“数据库落库”搅在一起。比如每次点“添加商品”都去执行一次UPDATE扣库存顾客中途删掉一行时你又得把库存加回来来回更新很容易出错。更稳的顺序是先只改动界面里的临时DataTable或List结账时一次性写入销售主表、销售明细细表并统一扣库存这三个动作还是走事务。下面这段代码演示结账前半段——把界面上的多行商品整理成DataTable并计算应收金额。private void btnCheckout_Click(object sender, EventArgs e) { DataTable saleDetail new DataTable(); saleDetail.Columns.Add(ProductId, typeof(int)); saleDetail.Columns.Add(Quantity, typeof(int)); saleDetail.Columns.Add(Price, typeof(decimal)); decimal totalAmount 0m; foreach (DataGridViewRow row in dgvCart.Rows) { // 跳过空行和整行未选中的行 if (row.IsNewRow || row.Cells[ProductId].Value null) continue; int productId Convert.ToInt32(row.Cells[ProductId].Value); int quantity Convert.ToInt32(row.Cells[Quantity].Value); decimal price Convert.ToDecimal(row.Cells[Price].Value); saleDetail.Rows.Add(productId, quantity, price); totalAmount price * quantity; } txtTotal.Text totalAmount.ToString(F2); txtChange.Text (Convert.ToDecimal(txtReceive.Text) - totalAmount).ToString(F2); }这里有个很实用的小点先把界面行转换成DataTable后续写销售明细时直接用这个DataTable作为参数传给存储过程或循环调用代码比一个格子一个格子读要清楚。Convert.ToInt32做的是显式类型转换源码里如果用的是as或直接赋值遇到DBNull时会报“指定的转换无效”这也是收银界面报错的高频原因。找零计算放在落库之前是没问题的因为付款金额与应收金额只和界面数据有关和数据库无关真正决定成败的落库动作要放在确认收款之后再执行。5. 权限、报表与打印源码中三块最容易返工的地方5.1 角色权限不要只写在按钮的if里很多超市管理系统源码的权限控制长这样在窗体的Load事件里判断if (CurrentUser.Role 管理员) { btnDelete.Visible true; }看起来能用但新增一个角色时所有窗体的判断都要改一遍。更常见的做法是把权限查询独立成一个方法判断当前用户有没有某个权限点的代码用“模块操作”的形式识别。比如“商品-删除”“销售-作废”各算一个权限点在数据库里用一张权限表维护。权限点编码模块操作Product_Add商品管理新增Product_Delete商品管理删除Sale_Cancel收银作废单据Stock_Adjust库存盘点调整改源码时不用大动只需要在按钮点击事件开头加一个校验方法校验不通过就提前返回。private bool HasPermission(string permissionCode) { if (CurrentUser null) return false; if (CurrentUser.Role 超级管理员) return true; string sql SELECT COUNT(*) FROM RolePermission rp JOIN UserRole ur ON rp.RoleId ur.RoleId JOIN SysUser su ON ur.UserId su.UserId WHERE su.UserId userId AND rp.PermissionCode code; int count Convert.ToInt32(SqlHelper.ExecuteScalar(sql, new SqlParameter(userId, CurrentUser.UserId), new SqlParameter(code, permissionCode))); return count 0; }这段代码把权限判断变成一次可复用的公共调用。源码里常见的错误是在多个窗体复制同一段权限判断逻辑后续改角色表结构时漏掉一个窗体就会导致“有人点了删除没反应”。用公共方法之后按钮点击事件的写法简化为if (!HasPermission(Product_Delete)) { MessageBox.Show(无权限); return; }代码量明显下降也为后面接Web管理端做权限配置留了接口。5.2 报表用DataTable导出Excel别等用户催报表才临时加超市管理系统的报表导出功能一般在“每日销售统计”窗体里。源码里常见的做法是DataGridView另存为Excel而WinForms的DataGridView并没有内置的直接导出方法很多是逐行用Clipboard复制粘贴导出数据一旦带回车符或换行符就会串列。常见做法是自己生成一个带逗号分隔的文本文件。考虑到中文和环境兼容性下面给出一种适应性更好的做法。public static void ExportDataTableToExcel(DataTable dt, string filePath) { StringBuilder sb new StringBuilder(); // 先写列头再用 DataRow 逐行写数据 sb.AppendLine(string.Join(\t, dt.Columns.CastDataColumn() .Select(c c.ColumnName))); foreach (DataRow row in dt.Rows) { string[] cells row.ItemArray.Select(v v DBNull.Value ? : v.ToString() ).ToArray(); sb.AppendLine(string.Join(\t, cells)); } // 写成 .xls 扩展名Excel 打开时会提示格式不一致但内容可用 File.WriteAllText(filePath, sb.ToString(), Encoding.Default); }逻辑说明这里用制表符做列分隔而不是逗号是因为商品名称、地址等字段天然可能含中文逗号或英文逗号用Tab分隔能减少串列。参数说明dt.Columns.CastDataColumn()把列集合转成可枚举集合row.ItemArray.Select(...)负责把每列的值转成字符串并处理NULL。这个方法适合导出销售记录、库存报表等典型管理数据但导出格式本质是TXT不是真正的Excel二进制。这里要说明白源码若想生成真正的xlsx文件建议换成NPOI或ClosedXML库而不是在这个导出函数上继续堆功能。5.3 小票打印别在按钮里拼坐标小票打印是超市收银系统的标配。PrintDocument会用PrintPage事件通知你“要在哪一页画什么”而不是像设置窗体一样设置控件的Location属性。很多源码用Graphics.DrawString配合一堆固定坐标来打印小票这个思路本身没错但它没有考虑打印纸张宽度换一台打印机就出现偏移。常见做法是在PrintPage事件里先测量页面宽度然后把文本按行写入表头居中明细左对齐。具体到代码层面可以先用e.MarginBounds得到可打印区域再通过MeasureString动态计算行距。打印完成后设置e.HasMorePages false让打印机结束任务。private void printDocument_PrintPage(object sender, PrintPageEventArgs e) { float y e.MarginBounds.Top; Font headerFont new Font(宋体, 14, FontStyle.Bold); Font bodyFont new Font(宋体, 10, FontStyle.Regular); // 小票抬头居中 string title XX超市销售小票; float x e.MarginBounds.Left (e.MarginBounds.Width - e.Graphics.MeasureString(title, headerFont).Width) / 2; e.Graphics.DrawString(title, headerFont, Brushes.Black, x, y); y headerFont.GetHeight(e.Graphics); // 订单号等明细字段按固定列位置打印 e.Graphics.DrawString(订单号: orderNo, bodyFont, Brushes.Black, e.MarginBounds.Left, y); y bodyFont.GetHeight(e.Graphics); // 明细行循环画 foreach (var item in saleItems) { string line string.Format({0,-12}{1,-6}{2,8:F2}, item.Name, item.Quantity, item.Price); e.Graphics.DrawString(line, bodyFont, Brushes.Black, e.MarginBounds.Left, y); y bodyFont.GetHeight(e.Graphics); } e.HasMorePages false; }这段代码的要点是不要用PageWidth减去固定像素来算右边距而要用e.MarginBounds保证内容在可打印区域内string.Format里的-12和-6表示左对齐占位用于让商品名、数量、价格对齐。y变量每次都累加一行高度这样即使商品名很长折行后续行也不会重叠。这套做法对源码原有打印模块的改造量不大但能显著减少换打印机后小票错位的问题。6. 三处改动让超市管理系统的源码从能用变好用系统能跑、能收银、能打印之后接下来这三点是我二开时一定会补的它们不改变业务功能但实实在在影响店面日常操作。第一商品信息做本地缓存。超市收银要连续扫码每个商品都查一次SQL Server的延迟虽然只有几十毫秒但顾客排队时几十毫秒会被放大成可见的卡顿。常见做法是启动时把商品表加载到Dictionarystring, ProductInfo扫码查条码时先在缓存里找找不到再回源查询。注意缓存要有失效机制最简单的做法是在商品管理的“保存”按钮提交成功后清空缓存下次查询自动重新加载避免出现改价后收银台还按旧价格结算的问题。private static Dictionarystring, ProductInfo _barCodeCache; public static ProductInfo GetProductByBarCode(string barCode) { if (_barCodeCache null) { _barCodeCache SqlHelper.ExecuteDataTable( SELECT ProductId, BarCode, ProductName, SalePrice FROM Product) .AsEnumerable() .ToDictionary( row row[BarCode].ToString(), row new ProductInfo( Convert.ToInt32(row[ProductId]), row[ProductName].ToString(), Convert.ToDecimal(row[SalePrice]))); } return _barCodeCache.TryGetValue(barCode, out var product) ? product : null; }这里要提醒一个代价ToDictionary要求条码字段不能有重复值否则启动就会抛“键已存在”的异常。所以代码里还要做一层防御比如存在重复条码时跳过该行并记入日志否则整个收银台都起不来。第二把扫码枪当作键盘输入处理。超市扫码枪默认就是模拟键盘扫描结果以回车结尾。在收银窗体的条码TextBox的KeyDown事件里判断Keys.Enter命中就直接添加商品并清空输入框这样不需要额外开发串口通信代码兼容性最好。源码里如果自带串口接收扫码枪数据那套方案尽量不要动因为串口方式改动风险大、回报不高。要注意的是别把业务逻辑写在TextChanged事件里TextChanged在每次按键都触发会把只扫了一个字母的商品也查一遍。第三写操作日志。任何业务系统上线后都绕不开“谁在什么时间删了什么商品”这种问题日志不一定要用Log4Net或NLog直接在删除、作废操作里插入一张日志表是最低成本的方案。有一个坑日志表写入失败不能影响业务成功否则库存已经扣了日志却回滚了反而更难查。正确做法是在业务事务提交后再把日志写入放到事务外的try/catch里宁可日志缺失也不能让主流程失败。代码写完后的验证方式不是反复点界面而是做一次简单的并发收银测试开两个收银窗体用同一个商品同时结账看库存最终扣减数是否等于销售流水合计。这个场景测过了源码才算真的可以放到店里用。本文还有配套的精品资源点击获取
返回列表