生产流程
产品被组织成三个层级:大箱码、小箱码和产品码,其中大箱码下关联有多个小箱码,每个小箱码下又关联有多个产品码。在扫码入库的流程中,机器读码器会一直扫描产品的二维码,而箱码需要人工手动干预进行添加,添加时机器不会停止运作。
难点在于产品的二维码是按照倒叙添加的,因为数据库中每个层级一张表格,表格之间相互关联,所以在插入数据时如果没有上级表格的副键会很难建立联系。解决方法有两个,一是建立临时表,等到大箱子的二维码存入到临时文件中时,再将临时文件插入到数据库中。二是每扫描到产品码就插入到数据库中,表格的副键设置为空,等到上一级二维码更新后再将表格中为空的数据更新。(下面的流程是以第一个方法为主,结果到客户那提了新需求后又只能用第二个方法了....
程序实现思路
整体分为三个步骤,提取扫码枪数据、建立临时文件存储数据、存入数据库。首先第一个步骤扫码枪的连接方式是USB所以读取数据非常方便,读码器会模拟键盘输入,只要用C#读取键盘输入数据就可以。第二步骤建立临时文件,临时文件使用.CSV文件或者.JSON文件,CSV更简洁JSON文件有层级类似于一个数组的样子,所以这里用的JSON文件因为数据量也不是很大。最后存入数据库就使用循环,每个产品都使用查询指令存入,不过好像批量存入可以用SqlBulkCopy。
数据库建表
三张表,每个表都有自增ID,而且大箱子会和小箱子的表中ID关联更新与删除
CREATE TABLE BigBoxes (
BigID INT IDENTITY(1,1) PRIMARY KEY, --自增ID--
BigCode NVARCHAR(255) NOT NULL UNIQUE, -- 大箱子吧,设为唯一
);
CREATE TABLE Boxes (
BoxID INT IDENTITY(1,1) PRIMARY KEY,
BoxCode NVARCHAR(255) NOT NULL UNIQUE, -- 箱码,设为唯一
BigID INT, -- 外键,关联大箱子表
CONSTRAINT FK_Boxes_BigBoxes FOREIGN KEY (BigID)
REFERENCES BigBoxes(BigID)
ON DELETE CASCADE -- 如果大的被删除,关联的箱也会被删除
ON UPDATE CASCADE, -- 如果大的ID更新,关联的箱ID也会更新
);
CREATE TABLE Products (
ProductID INT IDENTITY(1,1) PRIMARY KEY,
ProductCode NVARCHAR(255) NOT NULL UNIQUE, -- 产品码,设为唯一
BoxID INT, -- 外键,关联箱表
CONSTRAINT FK_Products_Boxes FOREIGN KEY (BoxID)
REFERENCES Boxes(BoxID)
ON DELETE CASCADE -- 如果箱被删除,关联的产品也会被删除
ON UPDATE CASCADE -- 如果箱ID更新,关联的产品ID也会更新
);
关于自增ID的问题,假如0自增ID到100而之前的数据被删除的话,下一次自增还会从100开始,于是使用下面的代码重置自增ID
DBCC CHECKIDENT ('boxes', RESEED, 0);
#重置boxes箱子中的自增ID为0
提取扫码枪数据
因为扫码枪读取数据就是模拟键盘输入,所以就用了一个简单的textbox控件来获取扫码枪的输入,其中要注意的就是焦点的变化,可以使用按钮来触发启动锁定焦点在文本框上。还有在第一次扫码输入到文本框后,下一次扫码要覆盖当前文本框的内容,这里因为扫码枪会在扫码后添加回车,所以事件用回车做判断,而且有一个记录器state来记录是否上次输入了回车,如果上次输入过回车就在输入前会把文本框清空再输入
技术变化的好快啊,搜扫码枪的时候还看到了通过串行接口来获取数据的方法好麻烦,用USB来获取数据的方式好方便
这一段是对一次扫码完成后的操作,当一次扫码后回车触发事件
private void Box1_KeyDown(object sender, KeyEventArgs e)//输入回车字符时触发事件
{
if (state == 1)//这个状态好像可以用布尔值来表示,因为只有0和1...
{
Box1.Clear();
state = 0;
}
if (e.KeyCode == Keys.Enter)
{
state = 1;//输入过回车就会记录下来,等到下次有按键触发时就会先清空
inputBuffer = Box1.Text;//inputBuffer变量来记录文本中的数据
richTextBox1.AppendText(inputBuffer + "\n");
richTextBox1.ScrollToCaret();
judge(); //这个是判断方法,后面用来根据输入的次数来判断是大小箱码
}
}
建立临时文件存储数据库
创建的JSON文件存储着产品码与小箱码的数据,最后读码获取的大箱码并没有存储,而是存储在一个变量中,到时候插入数据库的时候直接使用存储的变量作为插入语句。其实也可以存储在文件中,感觉这样好像比较规范,不过用变量存储的方法代码写起来比较方便就是了。
这一段是将数据添加到JSON文件中
using Newtonsoft.Json;
public void insertJson(string code, string now)//向json文件中加入数据,如果没有文件就新建
{
if (File.Exists(jsonFilePath))
{
//json文件的读取与反序列化
string json = File.ReadAllText(jsonFilePath);
ProductData productData = JsonConvert.DeserializeObject<ProductData>(json);
if (now == "product") //判断加入的是箱码还是产品码
{
productData.产品.Add(code);
}
else if (now == "box")
{
productData.箱码 = code;
}
// 保存修改后的数据
string output = JsonConvert.SerializeObject(productData, Formatting.Indented);
File.WriteAllText(jsonFilePath, output);
//数据序列化后写入文本
MessageBox.Show("文件写入成功");
}
else
{
MessageBox.Show("文件不存在!");
creatJson();
insertJson(code, now);
}
}
最初我也想过用.txt格式或者excel的文件来作为临时文件,就是txt文件中存储着产品码,文件名是箱码,存储在大箱子的文件夹中,因为只是起到一个临时存储防止断电或者重启数据消失的作用,而且对文件夹的操作也是自己熟悉的。。。
存入数据库部分
读取JSON文件并且使用查询语句插入到数据库表中,数据库语句感觉也挺麻烦的,存入大箱码后会自动生成ID,然后在存入箱码时声明一个变量存储大码的ID,再将ID与箱码对应存入表中,产品码也是,都是两层的循环。
using (SqlConnection sqlConnect = new SqlConnection(connectionString))
{
try
{
sqlConnect.Open();
MessageBox.Show(BigCode);
string query = $"INSERT INTO BigBoxes (BigCode) " +
$"VALUES('{BigCode}'); ";
SqlCommand sql1 = new SqlCommand(query, sqlConnect);
int result = sql1.ExecuteNonQuery(); // 执行 SQL 命令
foreach (var filepath in filesPath)
{
#region 箱码插入
// 读取文件内容
string jsonContent = File.ReadAllText(filepath);
// 反序列化JSON到ProductData对象
ProductData productData = JsonConvert.DeserializeObject<ProductData>(jsonContent);
string box = productData.箱码;
query = $"DECLARE @BigID INT " +
$"select @BigID = BigID from BigBoxes " +
$"where BigCode = '{BigCode}'; " +
$"INSERT INTO Boxes(BoxCode , BigID) " +
$"VALUES('{box}', @BigID); ";
SqlCommand sql2 = new SqlCommand(query, sqlConnect);
int result1 = sql2.ExecuteNonQuery(); // 执行 SQL 命令
if (result1 > 0)
{
MessageBox.Show("箱码插入成功!");
}
else
{
MessageBox.Show("数据插入失败或未插入任何行。");
}
#endregion
#region 产品码插入
if (productData?.产品 != null)
{
foreach (var product in productData.产品)
{
query = $"DECLARE @BoxID INT " +
$"select @BoxID = BoxID from Boxes " +
$"where BoxCode = '{box}'; " +
$"INSERT INTO Products(ProductCode , BoxID) " +
$"VALUES('{product}', @BoxID); ";
SqlCommand sql3 = new SqlCommand(query, sqlConnect);
int result3 = sql3.ExecuteNonQuery(); // 执行 SQL 命令
if (result3 > 0)
{
MessageBox.Show("产品数据插入成功!");
}
else
{
MessageBox.Show("数据插入失败或未插入任何行。");
}
}
}
}
#endregion
sqlConnect.Close();
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
第二种方法不建临时文件直接存入数据库
提示:在用这种方法前要把数据库的副键ID设置为可以为空
insert into Boxes (BoxCode) values("要关联的箱码");
DECLARE @Box1ID INT
select @Box1ID = BoxID from Boxes
where BoxCode = "要关联的箱码";
#下面更新代码来建立产品码与箱码之间的连接
update pr
set BoxID = @Box1ID
from Products pr
where pr.BoxID is null;
小结
这只是第一版的程序,最终到客户现场又添加了很多需求,改了好多功能。另外并没有添加与PLC通讯并读取读码器扫取的产品码的部分,还有数据查重的部分。最后程序到客户手中,还会被客户发现操作不规范导致的各种各样的BUG,最后需要在程序中添加防呆功能来防止扫描二维码混乱。