简介针对企业ERP、MES、HRMS及进销存等制造类内部系统开发的Delphi三层框架完整提供基于DataSnap架构的服务端与客户端源码。服务端未引入任何第三方控件常用配置参数存于SQLite客户端采用MS SQL数据库并集成DevExpressVCL与FastReport报表控件整体无权限限制适合有一定基础的中高级Delphi开发者直接学习或二次开发。压缩包共415个文件约73.27MB主要包括115个pas单元源码、93个dfm窗体定义、29个dll动态库、17个fr3报表模板以及相关dpr工程、xml配置、sql脚本和ico图标等目录结构清晰便于按模块查找。目前已有4429人学习。框架最大价值在于将多年企业ERP开发经验沉淀为简洁的常用函数与业务逻辑封装搭配完整演示工程、数据库脚本和报表设计可帮助读者快速掌握三层架构落地方法缩短项目启动周期。1. 这个delphi三层框架MES生产管理系统源码车间数据终于不再是Excel黑匣子上个月去一家机械加工厂看生产调度会车间主任手里拿的是Excel粘贴出来的在制表三个班次报上来的数字对不上设备实际开机状态全靠班长口头说。这不是管理问题是生产数据没有在系统里流转。这也是我拆完这套delphi三层框架MES生产管理系统源码后最大的感受MES真正解决的不是自动化而是让每一笔工序报工、物料批次、质检记录都变成即时、可追溯、可以反查的数据。整套源码用Delphi实现标准三层架构客户端、中间层、数据库各司其职适合两类人一类是想看三层框架在生产场景下怎么落地的Delphi开发者另一类是工厂信息化负责人想找一个能改、能扩展的MES底座而不是买完就锁死的商业套件。2. 技术架构拆解Socket中间层下一张工单从点击到入库的完整链路MES这类生产系统有一个特点客户端数量不多但每个客户端都很重数据实时性要求高而且车间网络环境往往比写字楼恶劣——断线、丢包、跨网段是常态。这套源码没有选择DCOM或者HTTP而是用Socket作为中间层通信协议配合ClientDataSet的包机制来传输数据。这个选择很务实DCOM在跨网段和防火墙下配置极其痛苦HTTP方案部署轻但实时性和事务性不如Socket直接。我在实际项目里也踩过DCOM的坑最后换回Socket重写通信层血泪经验告诉我三层架构中间层选型稳定比炫技重要。2.1 三层如何分工客户端、中间层、数据库各自的边界先看整个工程的目录组织这部分能直接反映作者对三层边界的理解MESRoot ├── Client # 表现层VCL客户端 │ ├── Bin # 编译后的EXE │ ├── Forms # 主窗体、各业务窗体 │ └── Config # 客户端连接配置文件 ├── Middle # 中间层业务逻辑数据提供者 │ ├── AppServer # Socket中间层宿主程序 │ ├── Providers # DataSetProvider单元组 │ └── Business # 业务规则单元校验、计算 └── Data # 数据层 ├── SQLScripts # 建库脚本、初始化数据 └── StoredProcs # 存储过程报工、追溯查询从目录就能看出边界设计Forms里不放SQLProviders里不写业务算法存储过程统一在Data目录管理。客户端通过TSocketConnection连接中间层中间层用TDataSetProvider暴露数据集数据库只负责存储和受约束的修改。我见过很多自称三层的项目其实只是把ADOConnection挪了个地方SQL还是写在客户端这套源码的目录划分是真正把边界落到了工程结构上二次开发时只需要遵守这个目录约定就不会把三层重新写成两层。2.2 请求链路ClientDataSet如何穿越三层到达数据库客户端拉取工单列表的代码是整个链路最典型的入口// 客户端单元工单列表查询 procedure TMainForm.OpenWoList; begin // SocketConnection负责连接中间层地址和端口从配置文件读取 SocketConnection1.Host : FAppServerHost; SocketConnection1.Port : StrToInt(FAppServerPort); SocketConnection1.Connected : True; // ClientDataSet通过ProviderName找到中间层对应的DataSetProvider cdsWoList.ProviderName : dspWoList; cdsWoList.Close; cdsWoList.Open; // 触发远程调用中间层执行SQL并返回数据集 end;中间层对应的DataSetProvider单元里查询逻辑长这样// 中间层单元TDSPWoList 的 DataSetProviderBeforeGetData 事件 procedure TDSPWoList.DataSetProviderBeforeGetData(Sender: TObject); begin // 用工号和计划日期过滤数据避免一次拉全表 qryWoList.SQL.Text : SELECT wo_id, wo_code, line_name, plan_qty, status FROM mes_wo WHERE plan_date :d AND plan_line :line; qryWoList.Parameters.ParamByName(d).Value : FPlanDate; qryWoList.Parameters.ParamByName(line).Value : FLineName; end;这段链路里容易被新手忽略的是ProviderName只是一个字符串映射中间层必须有同名DataSetProvider存在否则客户端Open时直接报“Provider not found”。另外Open是同步阻塞调用如果表数据量超过几万行客户端界面会卡住常见做法是给ClientDataSet设置PacketRecords为500或1000配合FetchOnDemandTrue让数据分页拉取界面就不会被一次大查询堵死。参数里那个d和line是ADOQuery的参数中间层在执行SQL前会把值填进去这种写法比拼接字符串安全得多也避免车间工人输入单引号把整个查询炸掉。2.3 主从表提交ApplyUpdates为什么要把Delta反推回中间层MES里最典型的主从表就是生产工单主表一张mes_wo记录工单基本信息从表mes_wo_detail记录该工单经过的每道工序。客户端编辑完点保存提交代码是三层架构的精华// 客户端单元保存工单 procedure TMainForm.PostWo; begin // 先本地Post让修改进入Delta缓冲区 if cdsWoMain.State in dsEditModes then cdsWoMain.Post; if cdsWoDetail.State in dsEditModes then cdsWoDetail.Post; // ApplyUpdates把客户端累积的Delta发送给中间层 cdsWoMain.ApplyUpdates(0); cdsWoDetail.ApplyUpdates(0); // 提交成功后刷新界面 cdsWoMain.Refresh; end;ApplyUpdates参数0表示当前数据包-1表示全部包。主从表提交顺序有讲究必须先主后从因为从表外键依赖主表主键顺序反了会报外键冲突。中间层的DataSetProvider默认UpdateMode是whereAll也就是说提交时生成的UPDATE语句where条件会把所有原字段都带上这个机制能防止多人同时修改同一行时互相覆盖但代价是提交SQL变得很长。如果车间里对同一工单的并发编辑极少可以改成whereKeyOnly提升提交性能但代价是最后提交的人会静默覆盖前面的修改——生产系统里我一般保留whereAll多一次字段比对比丢数据强。Delta机制本质上是把客户端所有增删改操作打包成数据集发给中间层中间层再逐条解析执行这也是三层架构能实现瘦客户端的原因客户端根本不直连数据库所有SQL和事务都在中间层控制。3. 核心模块实战工单下达、工序报工、物料追溯怎么落的MES的业务主线很清晰从工单下达开始到工序报工记录产量良品不良品到物料批次绑定实现追溯最后到看板展示实时数据。这套源码把这四条线都串起来了而且每个环节都有对应的表和存储过程。我拆的时候最关注的是追溯模块因为追溯是MES区别于普通ERP的核心能力——它回答的是“这批货用了哪批料、过了哪些设备、谁在什么时间干的”这个问题。没有追溯的MES只是电子化台账有追溯才是生产数据的闭环。3.1 生产工单主从表一次下发的完整实现工单下发要解决的核心问题是一张工单包含多个工序每个工序又有自己的计划工时和标准产量客户端界面用主从两个网格展示中间层用一个DataSetProvider暴露两张表。中间层关键代码// 中间层工单主表Provider关联从表 procedure TDSPWoMain.DataSetProviderAfterGetData(DataSet: TDataSet); begin // 主表数据取出后自动携带从表明细 qryWoDetail.SQL.Text : SELECT detail_id, wo_id, process_no, process_name, plan_qty, std_hours, seq_no FROM mes_wo_detail WHERE wo_id :wo_id; qryWoDetail.Parameters.ParamByName(wo_id).Value : DataSet.FieldByName(wo_id).Value; qryWoDetail.Open; end;这段代码实现了主从数据集嵌套当客户端打开主表中间层根据当前wo_id自动把从表查出来客户端不需要二次发起请求。从表的关键字段是seq_no它决定工序执行顺序报工时按seq_no从小到大流转。还有一个容易忽略的字段是std_hours它不只是展示用第三章节的报工偏差计算会拿它和实际报工工时做对比超出阈值时在报表里标红。所以二次开发时如果要加新工序属性尽量在这个表加字段不要单独建关联表否则主从嵌套又要多写一层Provider逻辑。3.2 工序报工数据库事务里完成数量累加和在线库存扣减报工是整个MES里数据一致性要求最高的操作。一个工人扫一下工单条码输入合格数和不良数系统要做的事包括更新工单已完工数量、扣减当前工序在线库存、增加下一工序待加工库存、写入报工流水。任何一步失败都不能污染数据所以源码把这段逻辑放在存储过程里统一控事务-- 数据库存储过程sp_process_report CREATE PROCEDURE sp_process_report wo_id INT, process_seq INT, ok_qty INT, ng_qty INT, operator VARCHAR(20) AS BEGIN BEGIN TRANSACTION; -- 更新工单累计完工数量 UPDATE mes_wo SET done_qty done_qty ok_qty, ng_qty ng_qty ng_qty WHERE wo_id wo_id; -- 扣减当前工序在线库存 UPDATE mes_wo_detail SET wip_qty wip_qty - (ok_qty ng_qty) WHERE wo_id wo_id AND seq_no process_seq; -- 写入报工流水记录操作人和报工时间 INSERT INTO mes_report_log(wo_id, process_seq, ok_qty, ng_qty, operator, report_time) VALUES(wo_id, process_seq, ok_qty, ng_qty, operator, GETDATE()); COMMIT TRANSACTION; END;这个存储过程是中间层通过TADOStoredProc调用的客户端点报工按钮只传四个参数过去中间层不拼写SQL直接执行存储过程。把事务放在数据库层有一个好处即使中间层崩了数据库那边的事务要么全部提交要么全部回滚不会出现数量加了但流水没写的半截状态。报工参数里的operator必须从登录会话传入不能从客户端界面上让用户随便填否则追溯链会断。实际部署时我还会在这个存储过程里增加一道校验如果wip_qty扣减后出现负数直接RAISERROR回滚报工。数量为负的报工记录在制造业是不可接受的一旦出现整个追溯批次都会失去可信度。3.3 物料批次追溯正查与反查的两条SQL路径追溯模块提供两个方向的查询。正查是输入成品序列号向上查出用了哪些原材料批次反查是输入原材料批次号向下查出这批料流到了哪些成品上。核心是一张中间关联表mes_trace_link记录了原料批次和工序报工批次的绑定关系。反查SQL比较典型-- 反查物料批次号 - 成品序列号 SELECT trace.out_code, w.wo_code, w.line_name, r.process_seq, r.ok_qty, r.operator, r.report_time FROM mes_trace_link trace JOIN mes_report_log r ON r.log_id trace.report_id JOIN mes_wo w ON w.wo_id r.wo_id WHERE trace.material_batch B20240607001 ORDER BY r.report_time;正查就是反向再做一次表关联。这套表结构的核心设计是trace_link同时保存了原料批次和成品序列号而不是只保存一边再靠时间推断所以查询性能很好不需要递归CTE。字段里的material_batch是车间唯一的物料批次条码在生产启动时通过扫码枪扫入之后每一道报工都会携带这个批次号。做二次开发时如果工厂需要对接ERP的物料批次只需要在mes_trace_link上加一个erp_batch_id字段做映射不用改追溯主链路。3.4 设备看板与产量统计中间层聚合并推送给客户端看板的数据特点是读多写少而且需要定时刷新。源码里用Timer每隔30秒调一次中间层接口中间层执行聚合SQL返回给客户端网格和图表控件-- 看板数据按产线统计最近8小时产量和达成率 SELECT line_name, SUM(ok_qty) AS total_ok, SUM(ng_qty) AS total_ng, ROUND(SUM(ok_qty) * 100.0 / NULLIF(SUM(plan_qty), 0), 2) AS achieve_rate FROM mes_report_log r JOIN mes_wo w ON w.wo_id r.wo_id WHERE r.report_time DATEADD(HOUR, -8, GETDATE()) GROUP BY line_name;NULLIF(SUM(plan_qty), 0)是为了防止计划数量为零时出现除零异常这个细节在车间的真实数据里非常常见——新产线调试阶段经常有计划还没下但报工先过来的情况。看板接口我一般建议单独开一个只读连接串权限只给SELECT避免看板刷新频率影响到报工写入的性能。4. 运行部署从数据库脚本到客户端能点点点源码拿到手第一步不是打开Delphi编译而是先把数据库脚本跑通。我按这套源码的依赖整理了一条完整的启动路径照着走基本二十分钟内能让客户端连上中间层看到登录界面。4.1 环境清单与初始化部署需要准备的东西如下组件要求说明数据库服务器SQL Server 2008 R2及以上也可以用其他关系数据库需改ADO连接串中间层服务器Windows Server 2008及以上需安装对应版本Delphi运行库客户端机器Windows 7及以上需注册midas.dll网络客户端到中间层TCP可达默认端口19000数据库初始化直接执行Data目录下的SQL脚本脚本顺序不能乱先建库再建表最后插入基础数据。基础数据里包含车间、产线、工序字典和演示用的工单数据没有这些基础数据客户端下拉框会是空的登录进去什么都干不了。执行脚本时注意数据库排序规则选Chinese_PRC_CI_AS否则中文乱码和排序问题会在后续使用中反复折腾。4.2 中间层发布与配置中间层宿主程序编译后是一个独立EXE双击能跑起来但正式部署应该注册成Windows服务。关键配置是中间层的数据库连接串和Socket监听端口配置文件长这样[Database] DriverSQL Server Server192.168.1.100 DatabaseMESDB Usersa Password****** [Socket] Port19000 MaxConnections200这里MaxConnections直接决定车间同时在线人数上限默认200如果产线班组规模大需要调高但同时要考虑数据库连接池压力。中间层启动后在客户端机器上用telnet命令测一下端口通不通网络不通时查Windows防火墙入站规则这是最常见的第一道坎。4.3 客户端配置与启动验证客户端首次启动前需要修改Client目录下Config里的连接配置文件[Connection] Host192.168.1.101 Port19000 Timeout30Host填中间层服务器IPTimeout是Socket连接超时时间车间网络如果延迟高可以调到60。配置完成后启动客户端看到登录框说明Socket层已经通了。如果卡在“正在连接中间层”先用telnet测端口再确认中间层EXE确实在运行最后看中间层日志有没有报错——这套源码在中间层写了一个简单的日志文件每次客户端连接和断开都有记录排查问题非常有用。客户端分发时要注意每一台客户端机器都要注册midas.dll否则运行到打开数据集时会报“类未注册”。常见做法是写一个批处理在客户端启动前用regsvr32注册DLL。杀毒软件有时会拦截中间层的Socket监听部署时要把EXE目录加入白名单这个坑在车间电脑上出现频率极高。5. 避坑指南三层框架下的5个高频翻车点Delphi三层架构的坑很多不是语法问题而是环境、版本、部署顺序带来的玄学问题。我把拆这套源码时反复踩过、以及在现场帮别人排查过的典型问题整理成五条每一条都是真实发生过的翻车记录。5.1 部署与连接类中间层起不来、客户端卡死第一个高频问题是客户端打开时提示“Provider not found”。现象是ClientDataSet的ProviderName指向了一个中间层不存在的数据集名字。原因大多是中间层改了单元名或者复制代码时漏掉了Provider组件字符串映射找不到目标。解决方法是打开中间层工程在窗体设计器里确认DataSetProvider的Name和客户端Config里配置的ProviderName完全一致注意大小写和空格都要一致。第二个问题是客户端启动后长时间无响应然后报Socket连接超时。现象是点击登录按钮后转圈超过一分钟。原因一般是中间层服务器防火墙没放行端口或者客户端配置的IP是旧地址。解决方法是先telnet IP 端口如果通再检查中间层日志里有没有收到连接请求如果日志为空说明网络根本没到中间层基本就是防火墙或IP不对。我遇到过最离谱的一次是DHCP分配IP变了客户端配置还写死旧IP排查了半天才发现。第三个问题是midas.dll版本冲突。现象是系统在公司开发机跑得好好的部署到车间电脑上报“类未注册”或“内存访问冲突”。原因是开发机装过新版本Delphi注册了新版本的midas.dll而车间电脑注册的是旧版本甚至没注册。解决方法是在客户端统一使用源码附带的midas.dll版本用批处理强制regsvr32注册不要混用不同版本。5.2 数据与事务类乱码、主从表提交失败第四个问题是中文乱码。现象是客户端界面显示“锟斤拷”或“”集中在报工备注和工单产品名称字段。原因是数据库排序规则用了非中文的默认规则或者中间层ADO连接串里没指定字符集。解决方法是在创建数据库时指定Chinese_PRC_CI_AS连接串加上Character Set参数。这里有一个判断技巧如果是新建数据库乱码基本是排序规则问题如果是老数据库乱码更可能是连接串缺参数。第五个问题是主从表提交时报外键冲突。现象是新增工单时主表保存成功从表保存失败提示违反外键约束。原因是主从表ApplyUpdates顺序不对或者中间层的主从Provider关联没写对。这个坑最迷惑的地方在于开发环境单步调试时没问题因为主表Post后在内存里已经有主键而实际运行时提交顺序被打乱了。解决方法是在客户端代码里严格先ApplyUpdates主表确认返回成功后再ApplyUpdates从表不要图省事用一个循环同时提交。另外中间层主从Provider一定要配置好MasterSource关联否则从表不知道自己的外键值。除开上面五条还有一个容易被忽略的问题报工存储过程里如果用了GETDATE()而数据库服务器时区和车间不一致会导致看板统计的时间段错位明明八点报的工被算进七点那一档。解决方法是统一用应用服务器时间传入报工时间参数不要在数据库端取当前时间。6. 二次开发从框架层加一个新模块的完整路径拿到源码熟悉了结构之后最想做的事情往往不是改报表而是加一个全新模块。我建议走一条固定套路先建表再写中间层Provider最后加客户端界面。就以“设备点检记录”为例这个模块要记录每台设备每天的点检项目和结果大概五步能跑通。第一步在数据库执行建表脚本字段包括点检ID、设备编号、点检日期、点检项目、结果、点检人。第二步在中间层新建一个TDSPCheckList单元放一个TADOQuery写查询SQL再放一个TDataSetProvider把DataSet属性指到TADOQuery上。第三步在中间层主窗体上注册这个Provider确认Name拼接正确。第四步在客户端窗体上放一个TClientDataSet把ProviderName指向刚建的dspCheckList然后调用Open看数据能不能正常返回。第五步在客户端菜单加入口绑定到新窗体。走完这五步一个新模块的基本增删改查就通了整个过程不需要写任何数据库直连代码。对于熟手还有一个更省事的框架层技巧。这套源码的中间层调用是每个窗体各自放一个ClientDataSet每个都配连接串重复代码很多。我二次开发时习惯抽一个公共单元// 公共单元RemoteQuery封装中间层调用 function OpenRemoteQuery(AProviderName, ASQLFilter: string): TClientDataSet; begin Result : TClientDataSet.Create(nil); Result.RemoteServer : GSocketConnection; // 全局Socket连接 Result.ProviderName : AProviderName; Result.PacketRecords : 500; Result.Open; end;调用处只需要一行cds : OpenRemoteQuery(dspCheckList, WHERE check_date:d);这个封装的收益在新模块数量多了以后非常明显连接配置只维护GSocketConnection一处ProviderName集中管理任何中间层改名只需要在一处同步。从那以后我每次在框架里加新模块都强制走一遍这套流程先中间层Provider再客户端界面改完先看Open能不能通再写业务逻辑调试时间至少省一半。希望帮到你。本文还有配套的精品资源点击获取