news 2026/10/1 14:39:39

C# WinForm超市收银系统落地指南:SQL脚本、扫码结算与避坑技巧

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C# WinForm超市收银系统落地指南:SQL脚本、扫码结算与避坑技巧

简介:面向超市零售场景的 C# WinForm 收银/POS 系统完整源码包,适合 C# 初学者、课程设计或中小超市的信息化管理参考。系统覆盖商品管理、销售收银、库存预警、报表统计、多支付方式与用户权限等模块,并附带数据库初始化 SQL 脚本,可在 Visual Studio 中直接打开解决方案运行。资源共 90 个文件,以 28 个 C# 源码文件为核心,辅以 WinForm 设计资源(resx)、数据集定义(xsd/xss/xsc)、数据库实体模型(edmx)与 SQL 脚本,另含少量 DLL、配置文件及单元测试相关文件,整包约 12.02MB,结构清晰便于按功能分区学习。目前已吸引 87 人学习下载,说明具备一定的实用参考价值。通过该源码可完整了解超市收银系统的分层设计与数据库交互方式,既能直接用于毕业设计,也可在此基础上定制扩展。

1. 超市收营系统只有源码加个 SQL 文件就够了吗:一次讲清 C# WinForm 收银台的完整落地

“超市收营系统(源码+sql文件)-2025”这个标题,我在很多课程设计包里见过。它背后其实是三件东西:一套 WinForm 界面、一组 C# 业务逻辑、一个 SQL Server 数据库脚本。别把它想成大型系统,核心业务就是“扫码—算钱—记流水—管库存”。对刚学 C# 的人来说,这是最容易跑通的数据库桌面项目;对想接手改造的人来说,难点反而不在代码,而在 SQL 文件怎么导入、连接字符串怎么改、扫码枪焦点和回车怎么处理。这篇就把这三件事拆开讲,从建表、写登录、搭购物车,到附加数据库和避坑,按一条能照做的路子来。

2. SQL 文件该怎么设计:商品、会员、销售流水三张表一次建档

一个收银系统的 SQL 文件,不是把表建出来就完事。它要同时满足“扫码时快查商品”“结算时写单不丢数据”“统计时能按日期聚合”这三个需求。常见的源码包里数据库名会叫 Supermarket 或 SHOP_DB,下面这组脚本以 SQL Server 为目标,用最基础的语句写成,2016 到 2019 的版本都能直接跑。

2.1 商品表、会员表、销售流水表:三张核心表的建表脚本

第一张表是商品表。这里的 Barcode 字段要建唯一索引,因为扫码枪读的就是它;Price 必须用 DECIMAL(10,2),不要用 float。下面这段是完整的建库和建表脚本:

CREATE DATABASE Supermarket; GO USE Supermarket; GO CREATE TABLE Product ( ProductId INT IDENTITY(1,1) PRIMARY KEY, Barcode NVARCHAR(32) NOT NULL UNIQUE, Name NVARCHAR(100) NOT NULL, Price DECIMAL(10,2) NOT NULL, Stock INT NOT NULL DEFAULT 0, Category NVARCHAR(50) NULL, CreateTime DATETIME DEFAULT GETDATE() ); GO

说明:ProductId 用自增主键,好处是插入时不用自己算最大编号,并发下也不会重复。Barcode 加上 UNIQUE 约束后,数据库层面就拦住“同一条码两件商品”的情况。Stock 默认 0,防止新增商品时忘了填库存导致负数。Category 允许 NULL,收银时并不需要它,只有做报表按类汇总时才用得到。

接着建会员表和销售流水表。会员表一般只存手机号、余额、积分;销售流水要拆成主表和明细表,因为一张小票对应多个商品,只放一张表会导致大量重复的订单号和金额数据,统计起来又慢又容易出错。

CREATE TABLE Member ( MemberId INT IDENTITY(1,1) PRIMARY KEY, Phone NVARCHAR(11) UNIQUE NOT NULL, Name NVARCHAR(50) NOT NULL, Balance DECIMAL(10,2) DEFAULT 0, Points INT DEFAULT 0 ); GO CREATE TABLE SaleOrder ( OrderId INT IDENTITY(1,1) PRIMARY KEY, OrderNo NVARCHAR(20) NOT NULL, MemberId INT NULL, TotalAmount DECIMAL(10,2) NOT NULL, PayTime DATETIME DEFAULT GETDATE(), Operator NVARCHAR(50), FOREIGN KEY (MemberId) REFERENCES Member(MemberId) ); GO CREATE TABLE SaleDetail ( DetailId INT IDENTITY(1,1) PRIMARY KEY, OrderId INT NOT NULL, ProductId INT NOT NULL, Qty INT NOT NULL, Price DECIMAL(10,2) NOT NULL, SubTotal DECIMAL(10,2) NOT NULL, FOREIGN KEY (OrderId) REFERENCES SaleOrder(OrderId), FOREIGN KEY (ProductId) REFERENCES Product(ProductId) ); GO

这里有三点要注意。OrderNo 是业务编号,比如 “SO20250101120000”,别用自增主键代替,因为客户退货或对账时需要看一个可读的单号。MemberId 允许 NULL,表示“非会员买单”,如果用 NOT NULL,游客就没法结账了。SaleDetail 里的 Price 字段要单独存成交价,不要 JOIN Product 取现价,否则你改商品价格后,历史小票金额就跟着变了,这是做日结报表时最容易出幺蛾子的地方。

2.2 索引、事务与参数化查询:和超卖、SQL 注入对抗的三个设计

建好表之后,很多源码包就停在这了,但实际跑起来会发现两个问题:一是销售表数据过万后按日期查日结明显变慢,二是多台收银机同时结账时库存可能被减成负数。解决慢查询,只要在 PayTime 和 OrderId 上建两个普通索引:

CREATE NONCLUSTERED INDEX IX_SaleOrder_PayTime ON SaleOrder(PayTime); CREATE NONCLUSTERED INDEX IX_SaleDetail_OrderId ON SaleDetail(OrderId); GO

PayTime 索引服务的是“查今天卖了多少钱”这类聚合 SQL,OrderId 索引服务的是“按小票号找明细”。这类索引在 INSERT 时会有微小开销,但对收银系统这种读多写少的场景,收益远大于成本。

解决扣库存问题,需要在一个事务里同时完成“插入主表、插入明细、扣库存”三个动作,并且扣库存时要带上 Stock 数量的条件判断:

BEGIN TRANSACTION; INSERT INTO SaleOrder(OrderNo, MemberId, TotalAmount, Operator) VALUES('SO20250101120000', NULL, 100.00, 'admin'); DECLARE @OrderId INT = SCOPE_IDENTITY(); INSERT INTO SaleDetail(OrderId, ProductId, Qty, Price, SubTotal) VALUES(@OrderId, 1, 2, 50.00, 100.00); UPDATE Product SET Stock = Stock - 2 WHERE ProductId = 1 AND Stock >= 2; IF @@ROWCOUNT = 0 BEGIN ROLLBACK TRANSACTION; THROW 50000, '库存不足', 1; END; COMMIT TRANSACTION;

这段 SQL 直接拿到源码里就能当模板用。关键在 UPDATE 语句的 WHERE Stock >= 2:如果库存不够,这一行影响 0 条,我们就能回滚整个事务;否则先插单后扣库存,多台收银机同时操作时,后一台读到的是旧库存,就可能超卖。这里还有一层容易忽略的坑:如果两个事务都更新同一行 Product,数据库会给它上锁,第二个事务要等第一个提交或回滚才继续,所以收银系统尽量不要在事务里写“先查库存再更新”的两步操作,而是直接用一个 UPDATE 完成判断。

最后提一句 SQL 注入。很多课程设计源码里喜欢这样拼 SQL:“string sql = 'SELECT * FROM User WHERE name=' + txtName.Text”,这在本地演示没问题,一旦有人输入1' OR '1'='1就能绕过登录。正确做法是 C# 端用 SqlParameter 参数化,把输入当值而不是当 SQL 语句。后面章节的代码全部按参数化写法给出,千万不要把用户输入直接拼进去。

3. WinForm 收银主界面怎么写:从登录窗体到扫码结算的事件链路

拿到源码后,你会看到几个窗体:LoginForm 负责登录,MainForm 是收银主界面,还有 ProductForm、MemberForm 这类管理窗体。这一章我们不照抄源码,而是把最关键的两条链路写清楚:登录怎么校验,扫码后商品怎么进购物车。

3.1 登录窗体:用户表校验和回车键登录

登录窗体的核心表叫 [User],注意 User 是 SQL Server 关键字,建表时要加方括号。下面给出建表和一行示例数据,密码列存的是 MD5 散列:

CREATE TABLE [User] ( UserId INT IDENTITY(1,1) PRIMARY KEY, UserName NVARCHAR(50) NOT NULL UNIQUE, UserPwd NVARCHAR(32) NOT NULL ); INSERT INTO [User](UserName, UserPwd) VALUES('admin', '21232f297a57a5a743894a0e4a801fc3');

上面的密文是 admin 的 MD5,拿到就能直接登录。C# 端在点击登录按钮时,用参数化查询去核对用户名和密码:

private void btnLogin_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtUser.Text) || string.IsNullOrWhiteSpace(txtPass.Text)) { MessageBox.Show("用户名和密码不能为空"); return; } string sql = "SELECT COUNT(1) FROM [User] WHERE UserName=@u AND UserPwd=@p"; using (SqlConnection conn = new SqlConnection(connStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@u", txtUser.Text.Trim()); cmd.Parameters.AddWithValue("@p", MD5Helper.Md5(txtPass.Text)); conn.Open(); int count = (int)cmd.ExecuteScalar(); if (count > 0) { this.Hide(); new MainForm(txtUser.Text.Trim()).Show(); } else { MessageBox.Show("用户名或密码错误"); } } }

逻辑说明:ExecuteScalar 返回查询结果第一行第一列,这里就是 COUNT 的整数值,比 0 大就放行。参数化的两行是关键,AddWithValue 会把 TextBox 内容当成一个参数,避免拼字符串带来的 SQL 注入。MD5Helper.Md5 是工具类里常见的一个静态方法,内部用 System.Security.Cryptography.MD5 生成十六进制字符串;你原来的源码里如果用的是明文密码,建议顺手改掉。

一个体验细节:超市收银员习惯输完密码直接按回车,所以应该在密码框的 KeyDown 事件里加同样的逻辑,或者把窗体的 AcceptButton 设为 btnLogin。我自己一般两种都做,避免有人用 Tab 键切换到登录按钮再按空格时卡一下。

3.2 商品检索:扫码枪在 TextBox 上回车触发搜索

扫码枪本质是一把键盘,扫到条码后快速输入数字并自动补一个回车。所以收银主界面通常是一个只读或普通 TextBox 作为扫码输入框,收到回车就去数据库查商品。注意这里不能放在 TextChanged 事件里,否则扫码枪逐字输入时你会触发十几次查询。

private void txtBarcode_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Enter) { SearchProduct(txtBarcode.Text.Trim()); txtBarcode.Clear(); e.SuppressKeyPress = true; } } private void SearchProduct(string keyword) { string sql = @"SELECT ProductId, Name, Price, Stock FROM Product WHERE Barcode = @barcode OR Name LIKE @pattern"; using (SqlConnection conn = new SqlConnection(connStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@barcode", keyword); cmd.Parameters.AddWithValue("@pattern", "%" + keyword + "%"); SqlDataAdapter da = new SqlDataAdapter(cmd); DataTable dt = new DataTable(); da.Fill(dt); dgvProducts.DataSource = dt; } }

这里有两层含义。扫码枪输入完整条码时,Barcode 等值匹配恰好命中;如果手工搜商品名,LIKE 模糊匹配也能用。参数 @pattern 里手写的百分号只属于搜索值,不是拼 SQL,所以没有注入风险。e.SuppressKeyPress = true 的用途是截断回车键默认行为,避免 TextBox 产生 Windows 提示音。

还有一个键盘焦点问题:扫码之后要把焦点重新放回 txtBarcode,否则收银员用鼠标点了一下 DataGridView,下一枪的字符就会打进主界面其他地方。最省事的办法是在 SearchProduct 末尾调用 this.ActiveControl = txtBarcode,或者在商品表格的 MouseUp 事件里手动转回输入框。

3.3 购物车与结算:用 DataTable 当临时购物车,减少数据库往返

收银台界面右侧通常是一个 DataGridView,显示当前这张小票的商品。如果每扫一件商品就往数据库插一条临时记录,取消结算时清理起来异常痛苦;更常见的做法是在内存里放一张 DataTable,结算成功后再一次性写进 SaleOrder 和 SaleDetail。

private DataTable cartTable; private void InitCart() { cartTable = new DataTable(); cartTable.Columns.Add("ProductId", typeof(int)); cartTable.Columns.Add("Name", typeof(string)); cartTable.Columns.Add("Price", typeof(decimal)); cartTable.Columns.Add("Qty", typeof(int)); cartTable.Columns.Add("SubTotal", typeof(decimal)); dgvCart.DataSource = cartTable; } private void AddToCart(DataRow productRow) { int productId = Convert.ToInt32(productRow["ProductId"]); DataRow[] rows = cartTable.Select("ProductId = " + productId); if (rows.Length > 0) { rows[0]["Qty"] = Convert.ToInt32(rows[0]["Qty"]) + 1; rows[0]["SubTotal"] = Convert.ToInt32(rows[0]["Qty"]) * Convert.ToDecimal(rows[0]["Price"]); } else { DataRow newRow = cartTable.NewRow(); newRow["ProductId"] = productId; newRow["Name"] = productRow["Name"]; newRow["Price"] = productRow["Price"]; newRow["Qty"] = 1; newRow["SubTotal"] = productRow["Price"]; cartTable.Rows.Add(newRow); } }

说明:cartTable 的列类型和数据库表对齐,ProductId 是 int,Price 是 decimal。查询已有行时用了一个简单的 Select 条件,因为 ProductId 来自数据库的自增列,不存在注入风险;如果你想写得更严谨,也可以改用 LINQ 遍历 cartTable.Rows。每次扫码加一件,Qty 加 1,SubTotal 重算,避免买了三罐可乐还要手动改数量。

结算按钮的逻辑就是把 cartTable 里的数据循环插入 SaleOrder 和 SaleDetail,并执行第 2 章那段事务脚本。实际操作里要注意,DataGridView 的列顺序不能擅自改动,否则 DataPropertyName 对不上,保存时所有值都是 0。绑定 DataTable 后,每一列的 DisplayIndex 和 DataPropertyName 都要在属性窗口里确认一遍。

4. 把源码和 SQL 文件跑起来:Visual Studio 附加数据库与连接字符串的五步操作

很多下载“源码+sql文件”的人,第一步就卡在数据库上。这里给出我调试同类项目时的五步套路:建数据库、执行脚本、确认身份验证模式、改连接字符串、启动程序。每一步都不难,但顺序错了会多折腾半小时。

4.1 先执行 SQL 文件:从 SSMS 到 sqlcmd 两种方式

拿到 sql 文件后,不要直接双击打开,先用 SQL Server Management Studio 连接本机数据库服务,然后按 Ctrl+O 打开该文件,再点击执行。执行前确认当前库是 master 而不是某个业务库,因为脚本开头有 CREATE DATABASE Supermarket。

如果你的机器没装 SSMS,用命令行也可以:

sqlcmd -S . -d master -i supermarket.sql

其中 -S . 表示本机默认实例,-d master 指定初始数据库,-i 后面跟 SQL 文件的路径。如果文件里没有 GO 分隔符,多段批处理可能只执行第一段,所以下载到的文件尽量保持原样,不要用记事本重新保存,防止编码被改成不带 BOM 的 UTF-8。执行后打开 SSMS 左侧数据库列表,能看到 Supermarket 就算成功。

还有一种情况,下载包里的文件不是 .sql,而是 .mdf 和 .ldf。这时不需要执行脚本,直接把 MDF 文件附加到 SQL Server 实例即可:右键“数据库”→“附加”,选择主数据库文件。MDF 文件的版本要和你的 SQL Server 匹配,旧版本附加到新版本一般没问题,反过来会报错“版本不支持”。

4.2 连接字符串:server=.;database=Supermarket;uid=sa;pwd=... 的坑

源码里连接字符串通常写在 App.config 或某个 DbHelper 类的顶部。最常见的写法是:

<connectionStrings> <add name="SupermarketConn" connectionString="server=.;database=Supermarket;uid=sa;pwd=123456;" providerName="System.Data.SqlClient" /> </connectionStrings>

C# 端读取方式:

using System.Configuration; private static readonly string connStr = ConfigurationManager.ConnectionStrings["SupermarketConn"].ConnectionString;

这段配置有四个容易踩的坑。第一,server 后面的 “.” 代表本机默认实例,如果你的 SQL Server 是命名实例,要改成“服务器名\实例名”。第二,uid/pwd 要求 SQL Server 身份验证必须开启,否则即使密码正确也会报“用户登录失败”,需要在 SSMS 里把服务器属性改成“SQL Server 和 Windows 身份验证模式”,并重启 SQL 服务。第三,如果本机用 Windows 验证能连,换到别的电脑就失败,多半是对方没建同名的登录名和密码。第四,XML 里的 & 符号要写 &,有些源码从网页复制下来这里已经乱了,Visual Studio 会报属性错误。

如果你只想在本机快速跑通,也可以把连接字符串改成:

string connStr = "server=.;database=Supermarket;integrated security=true;";

这种 Windows 验证方式不需要用户名密码,但发布到别的收银机上得重新配置,所以课程设计演示用没问题,真正部署还是用 SQL 身份验证更省心。

4.3 源码结构:Program.cs、登录窗体、主窗体的调用关系

一个标准 WinForm 收银系统的入口在 Program.cs 里,代码通常是这样:

[STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new LoginForm()); }

[STAThread] 必须保留,不然剪贴板、OpenFileDialog 这些组件会出诡异问题。Application.Run(new LoginForm()) 让登录窗体成为主线程的消息循环,登录成功后由登录窗体自己跳转到 MainForm。如果你改成直接 Run MainForm,登录校验就没了。

常见源码包里除了这两个窗体,还会有 ProductForm 商品管理、MemberForm 会员管理、ReportForm 报表。关心调用关系时,直接看各窗体构造函数里传了什么参数。比如 MainForm 的构造函数接受一个用户名,就是为了在界面上显示当前操作员,同时把权限信息传给后续窗体:

public MainForm(string operatorName) { InitializeComponent(); lblOperator.Text = $"操作员:{operatorName}"; }

这样你就能顺着“入口 → 登录 → 主窗体”这条线把整个项目读通了。如果你下载的源码没有 App.config,而是把连接字符串写死在每个窗体里,建议统一收敛到一个静态类,否则换数据库时要改十几个文件。这个静态类可以放在 Common 文件夹里,属性名就叫 ConnStr,所有窗体共用一处。

5. 收银系统避坑:连接超时、金额精度、扫码枪回车和中文乱码

这章记录我在调试超市收银项目时真正遇到过的几个问题,每一条都是“现象—原因—解决”。新手上路时对照着查,能省下大量时间。

5.1 现象:本机能连数据库,换到收银机就报“用户 'sa' 登录失败”

原因:目标 SQL Server 实例没有开启“SQL Server 和 Windows 身份验证模式”,或者没有创建 sa 登录名,再或者连接字符串里的密码和实际不一致。这三个原因按顺序排查。

解决:在 SSMS 里用 Windows 身份登录,服务器属性 → 安全性 → 勾选“SQL Server 和 Windows 身份验证模式”;然后在安全性 → 登录名里找到 sa,右键设置密码,并把状态里的“启用”改为 True。最后右键服务器选择重启服务。连接字符串里密码要写在 pwd= 后面,不要带引号,也不要有多余空格。

这里还有个典型衍生坑:收银机防火墙没放行 1433 端口。SQL Server 默认 TCP 端口是 1433,客户机连不上时先 telnet 收银机 IP 1433 试一下,不通就去防火墙入站规则里新增端口。另外,如果源码里连接字符串写的是server=.\SQLEXPRESS,而目标机器装的是默认实例,也会报“找不到服务器”,把实例名去掉只留一个点即可。

5.2 现象:买三件单价 9.99 的商品,总计显示 29.969999

原因:源码里 Price 用的是 float 或 double。十进制小数在二进制里表示不精确,多次累加后尾部就会出现 0.99999 这种残差。这是新手最容易忽略的地方,因为数据库里看到的是 9.99,可内存里已经变了。

解决:数据库字段改成 DECIMAL(10,2),DataTable 的列类型改成 decimal,C# 里所有金额变量声明为 decimal。如果已经有 float 数据,用 CONVERT 函数过渡:

UPDATE Product SET Price = CONVERT(DECIMAL(10,2), Price); ALTER TABLE Product ALTER COLUMN Price DECIMAL(10,2);

改完代码后,还要检查 DataGridView 的 DefaultCellStyle Format 设为 N2,不然界面上仍可能显示一堆小数。这里有个习惯:任何涉及金额的合计,都先 Convert.ToDecimal 再算,不要直接拿 DataGridView 的 Value 对象做加法,否则可能因为空值或类型不同直接抛异常。

5.3 现象:扫码枪扫一件商品,购物车里总是出现两条

原因:扫码枪默认在条码后发送一个回车键。如果 TextBox 的 KeyDown 处理了回车,同时窗体 AcceptButton 也指向“添加商品”按钮,那么回车会先触发 KeyDown 的 SearchProduct,再触发按钮的 Click,同一件商品就被加两次。

解决:在 KeyDown 里加上 e.SuppressKeyPress = true 截断回车消息,并保证扫码输入框的 AcceptButton 设置为空而不是添加按钮。我还习惯加一个防抖标记,在 500 毫秒内同一个条码不重复添加:

private DateTime lastScanTime = DateTime.MinValue; private string lastBarcode = ""; if (barcode == lastBarcode && DateTime.Now.Subtract(lastScanTime).TotalMilliseconds < 500) return; lastBarcode = barcode; lastScanTime = DateTime.Now;

这条能直接解决便宜扫码枪的物理抖动和重复触发。还有一个容易忘的点:扫码时如果输入法停留在中文状态,扫码枪输入的数字会被组词,甚至变成汉字。解决方法是把 txtBarcode 的 ImeMode 属性设为 Disable,同时不要在窗体的 KeyPreview 里重复处理回车,否则事件会触发两次。

5.4 现象:DataGridView 绑定 List 后,0 和 1 显示成数字而不是 CheckBox

原因:WinForm 的 DataGridView 绑定 List 时,只会根据属性类型自动创建文本列;bool 属性才能变成默认复选框,int 的 0/1 不会自动映射。这是很多人在搜索“winform datagridview 将 list 的一列0和1显示为checkbox”的根源。

解决:手动添加 DataGridViewCheckBoxColumn,并把列的 DataPropertyName 指向布尔属性。如果你的实体类里是 int,可以先加只读属性:

public bool IsActiveBool { get { return IsActive == 1; } }

然后在设计器或代码里设置:

DataGridViewCheckBoxColumn col = new DataGridViewCheckBoxColumn(); col.HeaderText = "启用"; col.DataPropertyName = "IsActiveBool"; dgvUser.Columns.Add(col);

保存时再把这个布尔值转回 int。这里有个细节:如果实体类同时暴露了 IsActive 和 IsActiveBool 两个属性,DataGridView 可能会自动把 IsActiveBool 也生成一列,导致重复。可以在自动生成列的事件里手动移除原始列,或者把 IsActive 标为 [Browsable(false)],只让布尔属性参与绑定。

5.5 现象:SQL 文件用 SSMS 打开执行后,中文商品名全部变成问号

原因:文件本身是 UTF-8 编码,而 SSMS 在部分系统区域设置下按 ANSI(GBK 或 CP1252)读取,导致中文字符乱掉。这也常见于从网页直接复制出来的 SQL 文件,因为网页保存时默认 UTF-8。

解决:用记事本打开该文件,另存为时选择“UTF-8 with BOM”编码,再让 SSMS 重新执行。BOM 会让 SSMS 正确识别 UTF-8。命令行方式可以用:

sqlcmd -S . -d master -i supermarket.sql -f 65001

这里 -f 65001 强制以 UTF-8 代码页读取输入文件。如果文件是 GBK 保存的,把 65001 换成 936 即可。这条命令在批量导入数据库时尤其管用,别忽略。另外,插入中文时最好在字符串前面加 N,比如N'可乐',这样即使数据库默认排序规则不是中文相关,也能正常存下来。

6. 给这套收银系统做三个加分项:日结报表、Excel 批量导入和界面美化

源码跑通只是第一步,想交作业或者面试展示,建议再花半天加三个小功能。第一个是日结报表,不用额外装报表控件,直接用 DataGridView 汇总。第二个是商品批量导入,把 Excel 表格塞进数据库,省去一条条录入。第三个是界面美化,三个参数让窗体看起来不像 2008 年的教程截图。

日结报表的偷懒做法是绑定一个存储过程,返回当天每个商品的销售数量、销售额,然后用一个 DataTable 求和:

decimal total = 0; foreach (DataGridViewRow row in dgvReport.Rows) { total += Convert.ToDecimal(row.Cells["SubTotal"].Value); } lblTotal.Text = "日销售额:" + total.ToString("N2");

这只是展示层的汇总,真正的日结数据还是要靠 SQL 聚合,否则跨天时报表会算重。SQL 端可以用WHERE PayTime >= DATEADD(day, DATEDIFF(day, 0, GETDATE()), 0)过滤当天零点,再按商品分组。

Excel 批量导入我一般用 ExcelDataReader,先读到一个 DataTable,再循环写入 Product 表。导入前记得做两条校验:条码为空直接跳过,重复条码用 UPDATE 而不是 INSERT,否则导了一半报主键冲突,连后悔药都没得吃。文件路径别写死,用 OpenFileDialog 让用户自己选,并加上后缀名过滤.xlsx|.xls。

界面美化的低成本组合是:背景色用浅灰蓝、字体换成微软雅黑、开启 DPI 缩放:

this.BackColor = Color.FromArgb(245, 245, 250); this.Font = new Font("微软雅黑", 10F); this.AutoScaleMode = AutoScaleMode.Dpi;

加上这三点,整个项目的观感会立刻接近真实商业软件。最后提醒一句:数据库的 SQL 文件一定要保留一份可重复执行的版本,不要只依赖附加了数据的 MDF。我做这类项目习惯每次改表都另存一个升级脚本,留着后悔药,遇到翻车时能回滚。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 14:39:29

一条命令搞定 A 股技术分析:Claude Code Skill---stock-analysis 实战体验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 14:38:53

Flink线上故障排查指南:CK超时、重启、积压与倾斜

1. 写在前面&#xff1a;这四个坑&#xff0c;我基本都踩过 做Flink实时计算的人&#xff0c;早晚都会碰到今天要聊的这四件事&#xff1a;Checkpoint超时、任务频繁重启、Kafka消息积压、数据倾斜。可以说&#xff0c;这四兄弟是线上Flink作业最常见的“送命题”&#xff0c;也…

作者头像 李华