1. 认识T100程序开发的基本盘
1.1 先搞清楚4GL和T100到底是什么关系
很多刚接触T100开发的朋友,第一反应往往是:“4GL是一种语言,T100是一个ERP系统,这俩怎么结合?”这个理解方向没错,但不够完整。T100从底层引擎到应用模块,都是基于4GL技术体系构建的,开发者的日常任务就是使用Genero 4GL语言(T100的开发语言核心)去编写扩展程序、定制报表、调整界面交互逻辑。所以做T100开发,本质上就是做4GL应用开发,只不过目标环境绑定在T100这套业务框架之内。
T100的常用开发场景大致可以分成三类:一是报表与打印程序,二是画面/交互程序,三是批处理与接口程序。这三类任务对应不同的程序骨架与工具链,但都有一个共同点——4GL的语法结构和数据库SQL高度融合,基本上你写的每一段核心逻辑都逃不开“取数-运算-输出”这三件事。我在实际项目里最深的体会是:4GL的程序并不难上手,难的是理解T100自带的那套框架约定和内置API,很多新手写出来的程序能跑,但一遇到权限、多公司、多语言这些企业级需求就露馅了。
T100程序开发适合谁学?适合已经有SQL基础、了解基本业务流程(生产、配销、财务任一模块)的开发者。如果你完全没有数据库概念,建议先补一补SQL和基本的数据建模知识再进来,否则理解起T100里面的字段联动、数据校验、多表查询会比较费劲。
1.2 项目文件结构与开发流程的底层逻辑
T100开发环境里,每一个程序单元通常由几个文件组成,最常见的组合是:
.4gl主程序文件:存放核心逻辑、流程控制、事件处理.per画面文件:定义表单布局、字段属性、画面区块.4fd表单定义文件(视具体版本而定):描述字段与数据库表的映射.xml或.4pp报表模板文件:用于报表布局设计
这里我要特别强调一个经验:不要把业务逻辑全部堆在画面文件里。初学者容易把字段校验、按钮事件、查询逻辑一股脑塞进.per的代码区,表面上看开发很快,但后期维护极其痛苦。我踩过的坑是,某次接手一个订单录入画面,画面上几十个字段的规则全部写在画面代码里,光是一个“客户编号变更后联动带出客户名称”的逻辑就分散在三四处,排查问题时来回跳转,效率极低。
合理的做法是:画面负责“展示与交互事件触发”,主程序负责“业务规则与数据处理”,两者通过事件名、字段名和调用接口解耦。这个分层逻辑和Web开发里的MVC是一个道理,只是形式不同。T100的画面程序通过ON ACTION、ON CHANGE、BEFORE/AFTER FIELD这些事件块去捕获用户操作,然后调用主程序里的处理函数,主程序再通过 SQL 或内置API去读写数据库。把规则集中在主程序里,后续改需求时只需要动一个文件。
2. 报表开发的硬核技巧
2.1 报表模板与数据准备:别一开始就调样式
T100报表开发,我最想提醒大家的一件事是:先搞定数据流,再动模板设计,顺序反了会浪费时间。
很多开发者的习惯是拿到报表需求先打开模板设计器,把表头、行高、字体调来调去,做得很漂亮,结果发现取数逻辑有缺陷,数据对不上,又回头改报表数据源,模板里的字段映射全要重做。这个弯路我走了不止一次,尤其是那种输出列多、分组层级多的复杂报表。
正确的顺序应该是:
- 确认报表的输入条件(筛选区间、单据状态、币种等),明确查询边界。
- 写数据提取逻辑,先在数据库工具里验证SQL结果,确保查询结果集准确无误。
- 确定分页与分组逻辑,比如按客户分组、按月份分组、按仓库分组,这一步决定了报表模板中
FOR循环的嵌套层级。 - 最后才打开报表模板,把数据字段拖进去,设置显示格式。
报表数据准备阶段常用到的SQL写法不外乎:多表JOIN、子查询、聚合函数(SUM/COUNT/AVG)、CASE WHEN条件转换。但要注意T100的环境里经常有“多公司”“多语系”的数据隔离需求,查询条件里一定要带上公司过滤字段,否则汇总数据会把不同公司的单据混在一起,对账时怎么都平不了。这个属于企业级应用的隐藏要求,文档里不一定会写得特别显眼,但实际业务一定绕不开。
2.2 分页、小计与导出:三个高频需求的实现套路
报表开发中最高频的三个功能需求分别是:分页打印、分组小计、导出文件。
分页打印方面,T100的报表是通过START REPORT配合OUTPUT TO REPORT来执行的,控制分页的常见做法是利用BEFORE GROUP OF和AFTER GROUP OF触发块。比如按“客户编号”分组时,在每个客户组的开头输出表头信息,在组尾输出该客户的汇总金额。这里有个细节:如果某个客户的数据跨页了,默认不会自动重复表头,需要在对应的触发块里重新输出列标题,否则第二页的读者根本看不清楚那几列数字到底代表什么。我见过业务部门拿着错位的报表来投诉,原因就是跨页后缺少列头提示。
分组小计的逻辑相对固定:
BEFORE GROUP OF customer_id DISPLAY "客户:" , customer_name DISPLAY "序号 料号 品名 数量 单价 金额" END GROUP AFTER GROUP OF customer_id DISPLAY "小计:", SUM(amount) END GROUP导出功能上,T100提供了数组转出到Excel或CSV的常见做法。通过内置的EXCEL相关函数或者EXPORT语法,把查询结果集的数组直接写出。有一点实战经验要分享:导出大数量数据时别一次性往内存数组里塞,否则客户端内存会吃不消。分批读取(比如每500行写一批)会稳很多,尤其是在导出几万行明细的场景下,差异特别明显。
2.3 报表性能优化的几个切入口
报表跑得慢,是T100开发绕不开的痛。最常见的瓶颈通常出在三个地方:
第一,查询语句没有利用索引。T100的查询条件如果写成WHERE item_desc LIKE '%关键字%'或者对日期字段做函数处理后再比较(比如WHERE TO_CHAR(due_date,'YYYYMM') = '202501'),数据库基本没法走索引,大表查询直接全表扫描,性能断崖式下跌。正解是:范围比较就老老实实用>=和<组合,日期就传日期参数,不做函数转换。
第二,循环内查询(N+1查询)。程序先取一张单据主表,然后在FOREACH循环里逐笔去查单身明细——这是最典型的性能杀手。单体还好,如果是批量处理几百上千张单据,光是查询次数就能把数据库连接池拖垮。优化思路是:把主表和明细表通过一次多表JOIN或者一次IN条件批量查出来,放进内存数组里再逐笔关联。
第三,报表输出本身的开销。输出文本行时频繁刷新页面也会影响性能。T100的报表引擎本身对输出做了缓冲,但如果你的DISPLAY语句里拼接了大量字符串计算(比如不断做CLIPPED、SFMT()格式化),仍然会拉低速度。建议把格式化的逻辑尽量前置到数据准备阶段,报表输出阶段只做简单的字符串拼接。
3. 界面交互设计的常用方法
3.1 表单画面设计:从布局到字段校验的要点
T100的画面设计基于.per文件,画面上通过区块(Layout)来组织字段。常用布局包括表格布局(Table)、网格布局(Grid)以及自由排列。实际项目中,我推荐的布局策略是:主表信息区用表格或自由布局,明细数据区用表格控件(TABLE),这样既能保证视觉效果整洁,又能让明细行的增删改操作比较顺手。
画面字段的属性设计有几个需要留意的点:
- 必填字段的设置。T100里可以通过字段属性
NOT NULL配合ON CHANGE事件里的校验逻辑来实现,但要注意区分“数据库必填”和“界面必填”。有时候数据库字段允许为空,但业务上要求用户必须录入,这时就不能依赖数据库层,必须在画面层做校验。 - 字段显示格式。金额、数量、日期、时间的格式必须统一。T100的格式掩码(Format Mask)写法和传统C语言风格类似,
####.&&表示金额带千分位,日期用YYYY/MM/DD,如果各程序格式不一致,用户在不同画面看到同一字段的显示方式不同,体验很差。 - 下拉选择(Option)与弹窗选择(Lookup)的选择。像仓库、客户编号这种需要从主数据中选择的字段,不要用自由输入框,尽量挂弹窗查询。这样既保证录入准确性,又避免业务人员背编码。挂弹窗查询的方式是设置字段的
Zoom属性并绑定对应的查询程序或SQL。
3.2 弹出窗口与多画面联动的几种做法
T100开发中,几乎每个业务画面都涉及弹窗。比如订单画面上点击“查看客户余额”,或者从料号字段弹出来的料品查询窗口。4GL实现弹窗的经典做法是通过OPEN WINDOW配合DIALOG语句,在子窗口里执行业务查询,返回选中结果后回填到主画面的字段中。
这里的编程套路非常固定:
OPEN WINDOW w_pop WITH FORM "query_form" ATTRIBUTE(STYLE = "Dialog") DIALOG ON ACTION (accept) ... CLOSE WINDOW w_pop END DIALOG联动场景中更复杂的点在于“主从表的行选联动”。比如在订单表头画面中,用户选中某一订单编号,下方明细表要联动刷新显示该订单的行项目。实现思路是:在明细表控件的ON CHANGE ROW事件中触发重新查询逻辑,把选中的主表主键值作为查询条件传入,重新填充明细数组并刷新表格。开发时要注意事件触发的时机——行选中变化和单元格内容变化是两个不同的事件,别搞混,否则会出现“点击某行却刷新成上一行的明细”这种诡异现象。
3.3 动态画面控制:显示、隐藏、只读的实用场景
T100的画面字段并不仅仅是静态展示,很多时候要根据业务规则动态改变状态。比如单据已经审核后,画面上的编辑按钮和输入框要变成只读;或者当订单类型是“退货”时,某些正项字段要隐藏。
常用的控制方式是调用画面字段的属性设置接口,比如field.visible = FALSE、field.readonly = TRUE、field.canmodify = FALSE。但要注意作用范围:表格(TABLE)里的列控制和普通字段控制不完全一样。表格列的显隐,需要在表格定义里给列起好名字,然后在程序中通过列名控制整列的可见性。此前我在某个项目中就遇过:想隐藏明细表里的“备注”列,用字段方式去控制,结果怎么都不生效,后来才发现列级属性要走TABLE COLUMN的接口。
动态控制逻辑比较复杂的画面,建议在程序里写一个统一的“界面状态刷新”函数:
FUNCTION refresh_screen_state() IF g_doc_status = "Y" THEN -- 已审核:锁住所有编辑字段 ... ELSE ... END IF END FUNCTION每次字段值变化、单据状态变化后调用这个函数,界面状态就始终遵循同一套规则,不会出现“这个字段在这个画面能改、在那个画面不能改”的混乱情况。
4. 业务逻辑与数据库操作的心法
4.1 SQL与4GL的融合技巧
4GL语言的一大特色就是SQL语句可以直接嵌入程序流程中,无需像传统开发那样拼字符串再执行。常见的写法包括:
SELECT customer_name, credit_limit INTO l_name, l_limit FROM customer WHERE customer_id = l_cust_id这种直接嵌入的写法,简洁、可读性好,是T100开发中的主力。但有一条经验很重要:动态SQL要慎用。
当查询条件数量可变、排序字段可变时,开发者会倾向于用LET sql_text = ...拼接出一整段SQL字符串,再通过PREPARE和EXECUTE执行。动态SQL虽然灵活,但存在两个风险:一是天然增加了SQL注入的安全隐患,虽然内部系统一般不会有人恶意攻击,但拼接时如果引号没处理好,遇到特殊字符直接报错;二是性能无法利用编译期优化,语句每次都要重新准备。我的经验法则是:能写好固定的SQL,尽量不要动态拼;确实需要动态条件时,用固定SQL加上条件分支组合,或者只对WHERE片段做有限的拼接,并严格使用参数占位符?。
取数后数据的处理方式也有讲究。单行查询用SELECT ... INTO,多行数据用FOREACH游标循环,或者先把数据集放到内存数组里再遍历。初学者要特别注意:SELECT没有查到数据时,NOT FOUND的处理一定要做,否则程序会直接异常中断。放一个行数判断作为保护,是开发时的好习惯。
4.2 事务控制与并发处理
T100里的写操作有单笔写入和批量写入两类场景。单笔写入(如订单新增、审核)倒还好,用START TRANSACTION到COMMIT即可。批量写入时,事务边界的控制就非常重要了。
我的建议是:大批量数据处理时,不要一把梭全部包在一个事务里,而是按业务批次(比如500笔或1000笔)进行分段提交。理由很简单:如果2万笔数据全部在一个事务里,中途如果有一笔数据出了异常要回滚,那前面所有处理过的数据全部白做,时间成本极高;而且大事务会长时间锁住相关数据表,导致其他用户操作被阻塞。
分段提交的大致逻辑:
DEFINE l_count INTEGER LET l_count = 0 FOREACH cursor INTO ... ... -- 执行更新/插入 LET l_count = l_count + 1 IF l_count MOD 500 = 0 THEN COMMIT WORK -- 重新开启事务 START TRANSACTION END IF END FOREACH COMMIT WORK并发处理是另一个容易被忽视的点。两张单据同时修改同一个单据编号,或者同一料品库存被两个程序同时扣减,会产生数据不一致。T100的并发控制一般依靠“乐观锁”的思路:在画面上保留数据的版本号或最后更新时间,提交时检查是否与数据库当前值一致,不一致则提示“数据已被他人修改,请重新查询”。这个机制在4GL里实现并不复杂,难的是开发时有没有这个意识。大量并发问题都是上线之后才暴露的,比如某制造企业月底盘点时集中过账,频繁出现库存扣减丢失,就是因为程序里没有做并发控制。
4.3 错误处理与用户提示的习惯养成
4GL程序的错误处理,最核心的是WHENEVER ERROR和WHENEVER NOT FOUND的全局设置。很多企业级应用崩溃的起因,就是某个SQL执行失败后没有捕获异常,程序直接中断,用户对着一个空白界面不知所措。
常见的处理套路是:
WHENEVER ERROR STOP -- 开发期 -- 或 WHENEVER ERROR CALL handle_error()但WHENEVER ERROR STOP只适合开发阶段调试,生产环境的程序必须给用户一个明确的提示,同时把错误详情写入日志,方便后续排查。我习惯在程序里建立一个统一的错误处理函数,捕获SQL错误码、错误消息,写入应用日志表,并且在画面上弹出一个友好提示框。
用户在操作时遇到错误反馈,最烦的提示是“系统错误,请联系管理员”——这等于什么都没说。好的提示应当包含操作建议,比如“单据日期不能大于当前日期,请重新选择”。这类业务校验在写代码时就应该多花功夫,用MESSAGE或ERROR输出明确的中文提示,能省掉后面大量的运维解释成本。这里我特别强调一句:别嫌校验逻辑琐碎,用户体验的口碑就是靠这些细节堆出来的。
5. 调试排查与问题定位实战
5.1 用好调试工具
T100开发环境的调试器(Debugger)是排查问题最重要的工具,支持断点设置、单步执行、变量查看。很多新手喜欢靠加DISPLAY语句打日志来排查,这种方式不是不行,但效率偏低。正确姿势是:先在关键逻辑处设置断点,观察变量值的变化轨迹,确认是哪一步的数据不符合预期,再决定是否需要局部修改。
我整理了一套适合团队推广的排查流程:
- 复现问题,记录操作路径和输入值。
- 在可疑程序入口加断点,单步跟踪到异常发生点。
- 查看异常点相关的变量和SQL查询结果。
- 如果问题在SQL层,把SQL语句复制到数据库客户端单独执行,确认数据层面的表现。
- 修复后,除了验证正常路径,还要回归测试异常路径(比如无权限用户、空数据、边界值)。
这套流程看起来基础,但项目上大量排查效率低下,往往就是因为跳过了第1步,开发者直接凭猜测去改代码,改了N轮问题还在。
5.2 常见问题速查表
我在多个T100项目的开发与维护中,整理了一份高频问题速查表,分享给各位:
| 现象 | 常见原因 | 排查与解决思路 |
|---|---|---|
| 程序编译不过,报语法错误 | 漏写END IF、END FOREACH等结束符,或中英文标点混用 | 检查代码块完整性,重点看嵌套层级 |
| 查询结果为空,但数据库有数据 | 公司过滤条件不对,或语系/权限数据范围限制 | 检查查询条件中的公司、仓库、状态等过滤字段 |
| 报表数据重复或翻倍 | JOIN关联产生笛卡尔积,或明细表重复关联 | 检查多表JOIN条件,确认明细表关联的唯一性 |
| 画面打开报错 “Field not found” | 画面文件与程序代码字段名不一致 | 对照.per文件和.4gl中的字段定义 |
| 界面卡顿明显 | 查询未加索引,或循环内逐笔查询数据库 | 优化SQL,或把数据批量取到数组再处理 |
| 保存时报 unique constraint 错误 | 主键或唯一键重复,单据号重复 | 检查编码生成逻辑,确认是否用了重复的单据号 |
这张表不是凭空总结的,每个问题都是我实际处理过的案例中抽取的共性场景。如果你在项目上遇到类似现象,基本可以按表中的方向去定位。
5.3 我的几个独家排障心得
排障做多了,有些经验是文档里不会写的。我这里分享三则:
第一,T100程序里DISPLAY输出的提示不一定实时刷新到界面上,有时候用户和开发者会误以为程序没执行到某一步。排查时不要只依赖界面提示,要多看日志表和后台输出。
第二,遇到“这个程序在我机器上正常,在别人机器上报错”的诡异问题,第一反应不是怀疑代码,而是检查两边的客户端版本、环境变量、数据权限配置,尤其多公司环境里用户的数据权限不同,SQL查出来的集合会不一样,这是最容易被忽略的。
第三,批量修正历史数据的程序尤其要小心“只处理正常数据”的陷阱。真实脏数据里常有空值、重复值、异常状态码,你的程序如果只按理想逻辑写,碰到一条脏数据就可能中断整批任务。在写处理逻辑时,提前把数据分布查一遍,确认有没有空值、脏值,然后决定是跳过还是报错,远比上线后救火省心。
6. 程序设计方法论的沉淀
6.1 模块化开发:函数和库的复用
4GL程序开发初期,大家最自然的方式是一个界面写一个独立程序,代码一长就反复复制粘贴。复制粘贴用多了就出现一个问题:同一个业务规则(比如“计算税额”规则)在四五个程序里各实现了一遍,后来税率调整了,改一个漏一个,结果不同画面上算出来的税额不一致,财务对账直接崩。
最佳实践是:把高频、易变的业务规则封装成公共函数或函数库。比如税率计算、日期计算、单号生成、状态判断、异常信息提示,都可以集中管理。T100支持程序内的FUNCTION,也支持跨程序使用的库模块。建立一个公共程序库之后,各程序的代码量同步下降,维护效率提升明显。我曾经帮一个团队做代码重构,把原来散落在20多个程序里的“计算税额”逻辑统一收拢到一个公共函数里,后续政策调整时只改了这一个函数,所有画面和报表同步生效,那个感觉非常痛快。
模块化要注意函数命名规范和参数返回方式。团队里应该有统一的命名前缀约定,比如func_calc_tax()、func_get_next_docno(),看到名字就知道这个函数是干什么的,不要出现五花八门的命名风格,否则找函数比写函数还累。
6.2 从“功能实现”到“代码评审”的进阶路径
写代码写到一定阶段,瓶颈不再是如何实现功能,而是如何把代码写得更稳、更可维护、更易被团队接手。我见过很多开发者能把程序跑通,但是代码可读性比较差:变量命名为a、b、c居多,函数动辄两三百行,没有任何注释,流程图压根没画过。这种代码在开发阶段可能一切正常,但到了需要扩展功能或排查疑难问题时,维护成本会成倍放大。
我给团队做代码评审时,通常会关注以下几点:
- 命名是否达意:变量名、函数名是否一眼能看出用途,而不是需要顺着代码读下去才明白。
- 函数是否过长:一个函数超过80行往往意味着职责过多,需要拆分成多个小函数。
- 异常处理是否完整:SQL是否都有可能失败?
NOT FOUND的情况是否都处理了? - 事务边界是否清晰:每个修改操作是否明确知道自己在哪个事务内,什么时机提交。
- SQL是否正确利用索引:查询条件是否有隐式转换、是否有前导通配符的
LIKE。 - 画面事件是否都挂上了处理函数:有没有事件触发了但程序里找不到对应处理逻辑的隐患。
代码评审其实是一个高效的团队提升手段。被评审的人能发现自己意识不到的问题,评审的人也能从别人的代码里学到新套路。我比较建议项目团队在开发节奏允许的情况下,两周做一次代码走读,哪怕只是30分钟,坚持下来整个团队的代码质量会有显著变化。
6.3 一套好用的开发自检清单
在长期实践中,我沉淀了一份适合T100程序开发的自检清单。每次开发或修改完程序,提交之前过一遍清单,能有效减少低级的返工:
| 检查项 | 检查要点 | 自查结果 |
|---|---|---|
| 编译检查 | 无语法错误,无未定义变量 | 通过 / 不通过 |
| 数据检查 | 查询结果与业务预期一致,无重复无遗漏 | 通过 / 不通过 |
| 异常检查 | SQL失败、无数据、权限不足均有处理 | 通过 / 不通过 |
| 性能检查 | 关键查询用到索引,无循环内逐笔查询 | 通过 / 不通过 |
| 事务检查 | 事务提交点合理,批量处理有分段提交 | 通过 / 不通过 |
| 界面检查 | 字段格式统一,必填校验完整,状态控制正确 | 通过 / 不通过 |
| 日志检查 | 关键操作写日志,错误有详细记录 | 通过 / 不通过 |
这份清单我是在实际项目里反复修订出来的,最初只有四五项,后来踩了坑就补一项,慢慢形成完整版本。新人在团队里带的时候,直接拿这份清单去逐项验收自己的程序,上手速度快很多。
7. 关于T100开发,最后的几句经验之谈
T100程序开发这条路,前期主要靠熟悉语法和框架,中期靠积累业务理解和踩坑经验,后期拼的是工程化思维——模块划分、代码复用、性能意识、异常处理。我在多个项目实施中最大的体会是:开发能力固然重要,但真正拉开差距的是你对自己所写程序的掌控力。看到一个报错能快速定位是数据问题还是逻辑问题,看到一个报表能预判哪一步查询会成为性能瓶颈,看到一段旧代码能快速梳理出它依赖了哪些画面字段和数据库表——这些都是在反复实践中沉淀出来的。
有一件小事我一直坚持:每次解决一个疑难问题后,把排查过程整理成简短笔记,记下现象、原因、解决方案、涉及的SQL或函数。时间长了,这些笔记就是个人最宝贵的知识库,很多新问题翻一翻笔记就直接有了答案,不用重新踩坑。
最后再分享一个小技巧:写程序时养成同时在开发环境中打开数据库监控工具的习惯,程序一跑,SQL执行情况看得一清二楚。哪里慢、哪里查得不对、哪里循环查询次数异常,一目了然。这个习惯帮我省下了大量排查性能问题的时间,建议你也试试。