
简介面向从事激光打标软件二次开发与维护的.NET开发者这份金橙子防重码打标单机版工程围绕防止重复打标、MDB记录查询与设备端功能定制展开适合已具备C#/VB.NET基础、希望研究插件式打标软件架构的读者。压缩包共374个文件、约7.95MB包含157个jsf界面/资源定义、31个bmp图标、31个cs源码以及dll、plg插件、ini/config配置、resx/resources、exe程序和mdb数据库等可据此梳理解决方案结构、界面资源组织与设备配置链路。已有3003人学习下载。读者可结合源码分析打标流程、查询去重逻辑和插件扩展方式并参考现有工程进行报告生成、数据导入导出、界面定制、性能优化与错误排查快速建立可运行的二次开发与测试环境。1. 防重码不是加个 if金橙子打标二次开发卡在哪金橙子打标卡的二次开发真正的门槛从来不是把图形打到工件上而是保证同一个码只出现一次。做过追溯的人都知道激光一出光物理世界就变了数据库里的事务回滚不掉那一笔灰。某产线曾经在换班时把一批已经打过的码池重新推了一遍结果几十件产品带着重复二维码流到了客户手里最后靠人工逐件比对序列号才把货追回来。这类事故的根因往往不在打标卡而在上位机把「防重码」简化成一次查表判断既没有唯一约束兜底也没有把「打标中」这个中间状态落盘。这份二次开发资源解决的正是这一段——在打标卡 SDK 之上补一层状态机、幂等占位和断电恢复逻辑让打标动作具备可审计、可补打、可对账的能力。适合已经上手打标卡基础调用、准备把单机打标软件改造成产线工位程序的从业者。2. 打标链路怎么切从模板到卡控的四层结构2.1 打标卡 SDK 只是执行器防重码得自己兜很多人第一次接打标卡会下意识觉得「卡应该知道这个码打没打过」。事实相反随卡提供的接口基本是无状态的初始化、加载模板文件、按对象名改文本、下发出光指令、查询状态就这几类。它不知道业务上的批次、工单、补打审批更不知道这个字符串在半小时前是不是已经被另一台工位打过了。所以防重码的判定必须发生在调用打标接口之前而且判定结果要以持久化记录的形式存在磁盘上而不是存在上位机的内存集合里。这里有个反直觉的结论防重码的难点不在「判断」而在「不可回滚」。查表判断谁都会写问题在于从「库里标记为打标中」到「激光真正打完」之间存在几秒钟的时间窗口这几秒里可能断电、可能急停、可能操作员直接关掉软件。窗口期内的状态如果没有被准确记录重启后系统就不知道那件产品到底打了还是没打。后面几章的状态机和恢复流程本质上都是在处理这个窗口。选型上还有一个容易被忽略的点不要把判定写进界面的事件回调里。工位程序往往一机多枪、一人多机判定逻辑散落在按钮点击里三个月后连写代码的人都不敢改。把它抽成独立的一层哪怕只是一个类库后面加补打、加审批、加对账都会轻松很多。2.2 四层链路界面、规则、模板、卡控把整条链路切成四层是我在类似项目里最常用的一种做法切完之后每个人负责的边界很清楚出问题也容易定位到层。层级职责不该做的事典型故障表现工位层扫码输入、操作员确认、状态显示、报警提示不做任何唯一性判断界面卡死、重复点击触发两次打标规则层码规范化、幂等占位、补打审批、状态流转不直接调用打标接口码被打两次、状态卡在打标中模板层加载 ezd 模板、按对象名改文本、回读校验不在运行时修改模板结构打出来还是上一件的码卡控层初始化句柄、下发打标、等待完成信号、异常复位不做业务判断卡被占用、打标超时、需要重启软件这样分层之后规则层可以单独写单元测试模板层可以用空打模式验证卡控层可以脱离业务用一个固定的测试码反复跑。整个系统的可验证性一下子就上来了这在产线现场比代码写得多漂亮重要得多。规则层放数据库还是放内存也是个要提前定的事。单工位、单进程、允许停机几分钟的场景本地 SQLite 完全够用一旦出现两个工位共用一个码池就必须换成带唯一约束的服务端库或者把码池集中到一台机器上。用内存 Set 做去重的方案我见过不止一次重启即失效两台机器之间还不共享属于典型的血泪经验。2.3 一次合格打标的七步时序顺序不能乱。尤其是第三步和第五步前者保证「没占到位就不许出光」后者保证「写进去的就是要打的」。我一般会把下面这个时序直接贴在工位机旁边让操作员也知道软件在干什么。扫码枪读入原始字符串触发工位层事件。规范化去首尾空格、统一大写、全角转半角、剔除不可见字符同时做长度和字符集校验。幂等占位带着批次号去码池做条件更新影响行数为零直接拒绝并给出可读原因。加载模板命中缓存则复用已加载的 ezd未命中则从只读目录加载加载后校验对象是否存在。改文本并回读写完目标对象后立刻把值读回来比对不一致就中止。下发打标并等待完成信号超过设定超时则进入待确认状态而不是直接判失败。回写结果成功写 OK失败写 FAIL 并保留错误码可选地再走一次相机或扫码枪回读。第七步里的「可选」其实是分水岭。只做软件层防重码成本低、上线快适合内部追溯一旦客户要求出货扫码可查就必须加回读环节因为激光打出来的条码质量受材料、焦距、速度影响软件认为成功不代表扫码枪能读出来。2.4 金橙子模板里的对象命名把变量名写进 ezd模板层最容易翻车的地方是对象名。金橙子的模板文件在编辑器里默认生成的对象名往往是「文本1」「文本2」这种现场一旦有人重新排版对象顺序变了代码里按下标取对象的写法立刻失效而且失效得很安静——不报错只是打出来的内容不对。我的做法是在模板里给所有需要动态替换的对象起固定名字代码只按名字操作不按下标。对象名类型用途约束Txt_Code文本主追溯码必须存在缺失即拒绝启动Txt_SN文本产品序列号可选缺失时降级为只打主码Txt_Date文本生产日期或班次格式在规则层拼好再写入Bar_Code条码一维码或二维码码制与尺寸改模板不改代码配套还有几条硬规矩模板统一放在只读目录按「产品型号_版本号.ezd」命名模板里的字体必须内嵌不要依赖工位机的系统字体否则换台机器排版就漂速度、功率、频率这些笔号参数在模板里调好代码里只在必要时覆盖不要在代码里硬编码一整套参数。模板一旦要改走版本号旧版本保留现场出问题能立刻切回去这就是后悔药。3. 防重码的核心实现唯一约束加状态机3.1 三张表定生死码池、流水、任务数据模型不需要复杂三张表足够撑起绝大多数产线场景。码池负责唯一性流水负责审计任务负责批次隔离。关键在于职责别混流水表长得像日志就让它老老实实当日志不要让它承担唯一性职责。-- 码池唯一性由主键保证状态由业务流转 CREATE TABLE t_code_pool ( code VARCHAR(64) NOT NULL, -- 规范化之后的码 batch_no VARCHAR(32) NOT NULL, -- 批次/工单号 status VARCHAR(16) NOT NULL DEFAULT PENDING, station VARCHAR(16) NULL, -- 占用它的工位 retry_count INT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL, mark_time DATETIME NULL, PRIMARY KEY (code), KEY idx_batch_status (batch_no, status) ); -- 流水只做审计允许同一码多条记录首次打标、补打、失败重试 CREATE TABLE t_mark_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(64) NOT NULL, station VARCHAR(16) NOT NULL, action VARCHAR(16) NOT NULL, -- MARK / REPRINT result VARCHAR(16) NOT NULL, -- START / OK / FAIL / UNKNOWN err_msg VARCHAR(255) NULL, ts DATETIME NOT NULL, KEY idx_code_ts (code, ts) );主键选code而不是自增 ID是为了让「重复插入必然失败」这件事由数据库来保证而不是靠应用层先查再插。先查再插在并发下有一个经典的空窗两个线程同时查都说不存在然后都插进去了。status的取值我固定成五个PENDING、MARKING、MARKED、FAILED、VOID。别小看这个值域约束很多时候问题就出在有人偷偷加了个DONE或者2进去对账脚本一跑全是异常。可以在数据库层加一个 CHECK 约束或者至少在建表注释里写死。retry_count是给自己留的证据。同一码重试次数超过阈值就该报警因为正常流程下它不应该被反复重试——要么卡有问题要么码池数据有问题两种情况都需要人介入。3.2 幂等占位先占坑再出光占位这一步是整个防重码的心脏。它必须满足两个条件原子性以及能明确告诉调用方「为什么不行」。用条件更新拿影响行数是我最推荐的方式它同时解决了并发和原因判定。// 幂等占位把“这个码能不能打”压缩成一次带条件的 UPDATE public MarkPermission TryOccupy(string code, string station, string batchNo) { using (var conn new MySqlConnection(_connStr)) { conn.Open(); using (var tx conn.BeginTransaction(IsolationLevel.ReadCommitted)) { // 只有 PENDING 才允许被占影响行数 0 说明已被别人拿走或状态不对 const string sql UPDATE t_code_pool SET status MARKING, station station, mark_time NOW() WHERE code code AND batch_no batch AND status PENDING; using (var cmd new MySqlCommand(sql, conn, tx)) { cmd.Parameters.AddWithValue(code, code); cmd.Parameters.AddWithValue(station, station); cmd.Parameters.AddWithValue(batch, batchNo); if (cmd.ExecuteNonQuery() 0) { tx.Rollback(); return MarkPermission.Deny(QueryDenyReason(conn, code, batchNo)); } } // 流水必须在出光之前落库否则断电后无法判断打过没打过 InsertLog(conn, tx, code, station, MARK, START, null); tx.Commit(); return MarkPermission.Allow(code); } } }code传进来的必须是规范化之后的值大写、去空格、去全角这一步在工位层做完。如果规范化放在占位之后就会出现「同一个码大小写不同被当成两个」的漏洞而且这种漏洞在测试阶段几乎发现不了因为测试数据通常都是规整的。station记录是谁占的多工位场景下非常有用。某工位卡死没释放运维一看就知道该去重启哪台机器不用挨个问。InsertLog放在事务提交之前是因为它的语义是「我打算打了」。这条 START 记录如果存在但找不到对应的 OK重启后的恢复逻辑就有了判断依据。QueryDenyReason返回的原因要具体到人能看懂码不存在、已被本工位打过、已被其他工位打过、属于其他批次、已作废。笼统的「操作失败」在现场会被骂因为操作员不知道该找谁。3.3 补打通道要单独开一条路补打是防重码方案里最容易破防的地方。为了省事很多实现直接在原流程上加个「强制打标」勾选框勾上就跳过所有校验。结果就是所有重复打标事故都能追溯到那个勾选框。正确做法是把补打做成一条独立链路它只做一件事在原记录基础上追加一条 REPRINT 流水并且要求三个输入——原码、工单号、当班负责人确认。三个都齐了才允许打缺一个就拒绝。原码的码池状态不回退到 PENDING只把retry_count加一这样即便补打失败也不会出现「状态回退后被另一个人正常占用」的混乱。参数上我一般设三条阈值同一码补打次数上限 1 次、补打时效 30 分钟内、超出时效必须走纸质审批再手工录单。前两条写进代码第三条写进作业规范。参数不是越严越好太严了现场会想办法绕过你那比不设还危险。-- 补打前的最后一道检查确认该码确实有成功记录且补打次数未超限 SELECT p.code, p.status, p.retry_count, (SELECT COUNT(1) FROM t_mark_log l WHERE l.code p.code AND l.action REPRINT AND l.result OK) AS reprint_ok FROM t_code_pool p WHERE p.code ? AND p.retry_count 1;这条查询返回空行就拒绝补打返回非空行再由程序判断reprint_ok是否为零。两次判断看起来冗余实际上是在防两种不同的错误前者防超次数后者防「补打成功但流水没写进去」的不一致。3.4 码池导入与清洗码池数据从哪来决定了后面一半的坑。常见来源是客户下发的 Excel、上游系统导出的 CSV或者现场用号段规则生成。三种来源的清洗策略不一样但有一条是共同的导入必须在一个事务里完成任何一行出错就整体回滚不要留半截数据在库里。导入时最容易漏的是重复检测。Excel 里出现两行一样的码导入脚本如果只用INSERT主键冲突会让整个事务失败这时候要能定位到具体是第几行冲突而不是抛一个笼统的异常。我一般会在导入前先跑一遍「组内重复 库内重复」的预检把冲突行单独输出成一份文件让人工确认之后再去重导入。号段生成的情况则要防另一件事多个工位同时申请号段。号段必须由中心节点统一分配每次分配一块比如一千个用完再申请。千万不要让每个工位自己按时间戳生成机器时间不同步的时候两个工位会生成出完全一样的号。4. 与打标卡对接状态机、超时与中断恢复4.1 初始化句柄只做一次全程复用打标卡的初始化和关闭都是有代价的操作反复 Initial/Close 会让卡处于不稳定状态现场表现为「打几十件之后就不出光了重启软件又好了」。正确的做法是在程序启动时初始化一次句柄用单例持有退出时统一关闭。如果初始化失败先看两件事卡是不是被编辑器软件占着以及驱动是否正常加载。打标卡在同一时刻通常只允许一个进程控制编辑器开着工位程序就初始化不了。加载模板也一样。同一个模板反复加载既慢又容易出问题我会做一个模板缓存键是模板文件路径加文件修改时间文件没变就直接复用已加载的句柄文件变了才重新加载并刷新缓存。这样现场改模板之后不用重启软件但也避免了每次打标都读一遍磁盘。4.2 打标线程只能有一条不管界面上有多少个按钮真正调用打标接口的线程必须有且只有一条。做法是让所有打标请求进一个串行队列工作线程从队列里取任务逐个执行。这一步用BlockingCollection或者Channel都行关键是别用lock去保护打标调用——lock只保证互斥不保证顺序而且一旦打标卡死锁会一直不释放整个界面跟着假死。超时设置要留出余量。一个常规的小幅面二维码从下发到打完通常在几百毫秒量级但材料不同、功率不同、打标范围不同差别会很大。我一般按实测节拍的两到三倍设超时宁可多等一会儿也不要因为超时误判失败。超时之后不要立刻调用关闭或复位接口先查一次卡状态确认是真的卡住了再走复位流程。4.3 返回值不算数要闭环判定打标接口返回成功只代表指令下发成功不代表工件上真的有码。这句话我每次做培训都要强调一遍。判定要三层接口返回码、打标完成信号、物理回读。// 打标执行器串行调用三层判定超时进待确认而不是直接判失败 public MarkResult Execute(string code, string templatePath, int timeoutMs 3000) { if (!_card.Ready) _card.Initial(); // 句柄初始化一次后续复用 _card.LoadTemplate(templatePath); // 命中缓存则内部直接返回 if (!_card.SetTextByName(Txt_Code, code)) // 按对象名写入不按下标 return MarkResult.Fail(对象写入失败检查 Txt_Code 是否存在); if (_card.GetTextByName(Txt_Code) ! code) // 写完必须回读 return MarkResult.Fail(回读不一致模板对象名可能被改过); var sw Stopwatch.StartNew(); var rc _card.Mark(); // 仅代表指令下发 if (rc ! 0) return MarkResult.Fail($Mark 返回 {rc}); if (!_card.WaitDone(timeoutMs)) // 等完成信号超时语义是“未知” return MarkResult.Unknown($等待完成超时 {sw.ElapsedMilliseconds}ms需人工确认); return MarkResult.Ok(sw.ElapsedMilliseconds); }timeoutMs按实测节拍的两到三倍取别照抄默认值。MarkResult.Unknown这个状态很重要它和Fail的语义完全不同Fail表示确定没打上可以自动重试Unknown表示不确定必须人工看一眼工件再决定。把Unknown当Fail处理就是重复打标的源头之一。GetTextByName这一步看着多余实际能挡住一类很隐蔽的故障模板被别人重新排版过对象名被改了SetTextByName返回失败但有人把返回值忽略了于是打出来的还是模板里固化的旧值。加上回读这种问题在第一次打标时就会暴露。4.4 断电、急停、卡死三种现场怎么恢复三种中断的处理原则一样宁可让状态停在中间也不要猜。软件启动时先做一次扫描把所有status MARKING的记录挑出来连同对应的流水一起进人工确认队列界面上弹出提示要求操作员确认工件上是否已有码。确认有码的改判MARKED并补一条人工流水确认无码的退回PENDING允许重新打。急停的处理稍微不同。急停发生时打标可能已经出光了一部分这时候工件上是半个码。半个码的工件必须走作废流程status置VOID不允许退回重打因为同一个码打在两个工件上等于主动制造重码。这一点要写进作业规范不能只靠软件拦。卡死或者软件无响应最常见的原因是打标卡被异常占用。处理步骤是先停队列、再查状态、最后复位。复位失败就重启软件重启还不行再断电重启控制箱。别在卡死状态下反复点打标按钮那样只会让状态更难判断。恢复完成之后务必跑一次对账脚本把这段时间的流水和码池状态对一遍确认没有漏网的中间态。5. 避坑与排查五条现场记录5.1 现象打出来的还是上一件的码原因模板里对象名被改过或者用了按下标取对象的写法更隐蔽的一种是SetTextByName返回了失败但调用方没检查返回值程序继续往下走打出来的是模板里固化的上一次内容。解决所有写对象的调用都必须检查返回值并在写完之后立刻回读比对不一致就中止本次打标。模板上线前做一次对象名自检把Txt_Code这类必需对象列成清单启动时逐个检查存在性缺一个就拒绝进入就绪状态。这一条看似麻烦但它把一类随机性故障变成了启动时的确定性报错。5.2 现象补打之后出现双胞胎原因补打通道绕过了唯一约束操作员看到「打标失败」就点补打实际上第一次已经打上了只是完成信号没等到。软件判失败物理上却成功了。解决把「超时」和「失败」分成两个状态超时进人工确认队列而不是自动允许重打。补打必须带原码和工单号且要先查流水里有没有 OK 记录有 OK 记录的补打一律走审批。另外给补打加次数上限超过就锁死该码强制人工介入。这几个动作加起来能挡掉绝大部分双胞胎。5.3 现象重启后一堆「打标中」的僵尸记录原因占位成功、流水 START 落库之后程序在等打标完成信号时被强杀或断电状态停在MARKING上再没人管。时间一长这些码既不能重打也不能作废成了孤儿。解决软件启动时强制扫描MARKING状态全部转人工确认队列不允许静默转成 OK。同时对账脚本要能按时间窗口筛出停留超过 N 分钟的MARKING记录并告警。N 取多少看节拍常规产线 10 分钟足够慢节拍的工位可以放宽到 30 分钟。5.4 现象软件判成功扫码枪读不出来原因打标参数和材料不匹配。速度太快导致码点连接不清功率太低导致对比度不够焦距没调好导致边缘发虚。软件层只判断了指令下发没有判断光学效果。解决首件必须用实际使用的扫码枪在标准距离下读三次全过才算通过。参数按材料打样确定确定后写进模板版本不要在现场随手改。上线后每隔一段时间抽检一次抽检记录跟着批次走。有条件的话加一个固定位置的读码器做在线回读读不出直接报警停线这比事后追溯便宜太多。5.5 现象第二个工位抢不到打标卡原因打标卡同一时刻只能被一个进程控制。第一台机器的软件没退干净或者编辑器还开着第二台初始化必然失败。另一个常见原因是把两张卡装在同一台机器上驱动层面的设备序号没区分开程序总是打开第一张。解决初始化失败时给出明确提示区分「被占用」和「设备不存在」两种情况。多卡场景按设备序号或序列号显式指定不要依赖默认枚举顺序。工位程序退出时确保释放句柄异常退出要有兜底清理。这一条属于运维范畴但软件不给提示的话现场只能靠猜。6. 上线前的灰度验证与对账习惯防重码这种东西功能跑通只完成了一半剩下的靠验证。我一般的节奏是先在空打模式下跑通全流程也就是加载模板、改对象、不下发出光只走状态机。这个阶段跑五百件重点看码池状态流转是否正确、流水是否成对出现、异常分支有没有漏。空打模式的价值在于它把「光学」这个变量摘掉了剩下全是软件逻辑出问题一定是代码问题。接着做断电测试。跑到第两百件左右直接拔电重启后看人工确认队列里是不是正好有那一件数量对不对得上。这个测试至少要重复三次因为断电时机不同暴露的问题也不同。再往后是并发测试两个工位同时抢同一批码池看有没有出现同一个码被两个工位都占成功的情况。最后是对账。下面这段脚本我每次上线都会挂到定时任务里每天早上开班前跑一次输出为空才允许生产。# 每日对账三类异常必须清零否则不允许开班 import sqlite3 conn sqlite3.connect(mark.db) cur conn.cursor() # 1) 有 START 没有终态的码说明打标过程被打断 cur.execute( SELECT p.code, p.status, p.mark_time FROM t_code_pool p WHERE p.status MARKING AND p.mark_time datetime(now, -30 minutes) ) stale cur.fetchall() # 2) 同一码出现多条 OK 流水典型的双胞胎 cur.execute( SELECT code, COUNT(1) AS ok_cnt FROM t_mark_log WHERE result OK AND action MARK GROUP BY code HAVING ok_cnt 1 ) dup cur.fetchall() # 3) 有 OK 流水但码池状态不是 MARKED状态回写丢失 cur.execute( SELECT l.code FROM t_mark_log l JOIN t_code_pool p ON p.code l.code WHERE l.result OK AND p.status MARKED ) mismatch cur.fetchall() print(僵尸记录:, stale) print(重复成功:, dup) print(状态不一致:, mismatch) conn.close()三类异常对应三种不同的问题第一类查的是中断恢复有没有做全第二类查的是补打通道有没有被绕开第三类查的是事务边界有没有踩线。脚本本身很简单难的是坚持跑。我见过太多项目上线第一周天天看对账一个月之后就没人管了等到客户投诉重码才想起来翻日志。顺便说一个验收清单的用法把「首件扫码三次全过」「断电恢复数量一致」「并发抢占无重复」「对账三列为空」这四条列成表格每一条都要有人签字。签字这个动作看着形式主义但它把责任落到了具体的人身上比在群里发一句「测过了」有用得多。从那以后我每次接打标类项目都会在写第一行业务代码之前先把码池表和状态机定下来哪怕界面还没画。因为界面可以改状态一旦乱了追起来就是几天几夜。希望帮到你。本文还有配套的精品资源点击获取