1. 为什么我们需要自定义字段从“优先级”说起大家好我是老张在团队里捣鼓禅道快十年了。不知道你们有没有遇到过这种情况测试同学跑来抱怨说用例列表里密密麻麻分不清哪些是核心功能要优先保障的哪些是可以往后放一放的。产品经理也头疼每次评审都要口头强调“这个需求优先级高”但落到禅道的测试用例里这个信息就丢了全靠测试人员自己记。你看这就是一个典型的“信息断层”。禅道自带的测试用例模块字段很标准标题、步骤、预期结果……但它没有“优先级”这个字段。对于很多团队来说特别是项目多、迭代快的团队给测试用例打个“优先级”标签简直是刚需。高优先级的用例可以安排更资深的测试人员、在回归测试时优先执行、出问题时也优先排查。所以今天我们就来干一件特别实在的事给禅道的测试用例模块新增一个“优先级”自定义字段。别听到“二次开发”、“自定义模块”就觉得头大觉得要动底层框架、伤筋动骨。其实不然禅道的扩展机制zentaoPHP框架的核心设计之一就是为了让我们能“优雅地”加功能而不用去改它的核心代码。这就像给你的房子加个飘窗是在原有结构上做加法而不是把承重墙给砸了。接下来我会手把手带你走完整个流程从分析这个需求到底要改哪些地方到设计数据库字段再到利用禅道的ext扩展目录一点点把模型Model、控制器Control、视图View、语言包Lang、甚至前端的JS/CSS都给安排上。目标就一个让你不仅能跟着我做出来更能理解为什么这么做下次你自己想加个“紧急程度”或者“测试环境”字段时也能举一反三。咱们不搞虚的直接上实战。2. 动手前的准备理清思路与找准入口在写第一行代码之前咱们得先把“战场”勘察清楚。盲目动手很容易掉进坑里。2.1 需求拆解一个字段的“一生”咱们的目标是加一个“优先级”字段。别小看这一个字段它在系统里走完一圈需要经过好几个环节数据存储这个字段的值比如“高”、“中”、“低”最终要存到数据库的哪张表里字段类型是什么数据录入在哪里、用什么形式让用户填写这个字段这就是“新建用例”和“编辑用例”页面要干的活。数据展示填进去的数据得能在“浏览用例”的列表里看到吧这就是列表页的展示。数据处理用户点击保存后后台代码Controller怎么接收这个值并交给模型Model存进数据库反过来编辑的时候又怎么把数据库里的值读出来塞回表单里语言与样式页面上显示的“优先级”三个字以及下拉框里的选项需要多语言支持。可能还需要一点点CSS来调整样式。理清这个流程我们才知道要去修改哪些文件而不是像无头苍蝇一样乱撞。2.2 定位扩展的“坐标”模块与方法禅道所有的功能都按模块Module组织。我们的目标是测试用例那对应的就是testcase模块。怎么确认呢有个最简单的办法打开浏览器进入禅道的“新建用例”页面看一眼浏览器地址栏的URL。比如你看到的可能是http://你的禅道地址/zentao/testcase-create-1.html这个URL就是我们的“藏宝图”testcase这就是模块名。告诉我们相关的代码都在/module/testcase/这个目录下。create这就是方法名也叫操作名。对应的是control.php文件里的create方法负责处理新建页面的逻辑。1这是传给create方法的参数通常代表产品ID。所以我们这次扩展的核心阵地就是testcase模块的create新建、edit编辑和browse浏览这几个方法。视图文件就是HTML模板在/module/testcase/view/目录下分别对应create.html.php,edit.html.php,browse.html.php。2.3 认识我们的“工具箱”ext扩展目录这是禅道二次开发最精髓的部分我们绝对不应该直接去修改/module/testcase/下面的原始文件。为什么呢一旦禅道版本升级你改过的核心文件会被覆盖所有改动一夜回到解放前维护起来是场噩梦。正确的姿势是使用ext扩展目录。在每个模块下面都有一个ext目录如果没有就自己创建。这个目录是禅道官方预留的“扩展区”系统在运行时会主动加载这个目录下的代码并与核心代码合并。我们的所有修改都放在这里这样禅道升级时只要扩展机制不变我们的代码就安然无恙。以测试用例模块为例我们的扩展目录路径是/module/testcase/ext/。在这个目录下我们可以建立和核心目录类似的结构比如control/,model/,view/,lang/,js/,css/等。接下来我们所有的操作都将在这个“安全区”内进行。3. 从数据库开始给数据安个家功能再好数据没地方存也是白搭。所以第一步咱们得在数据库里给“优先级”这个字段腾个位置。3.1 设计字段打开数据库管理工具比如phpMyAdmin找到禅道数据库默认前缀是zt_查看zt_case表。这张表就是存储所有测试用例的。我们需要新增一个字段。字段名要清晰易懂我习惯用priority。类型的话优先级通常是一个固定的枚举值比如“高”、“中”、“低”用varchar或者char都可以。长度给个30足够。我们还可以设置一个默认值比如“中”medium。为了让表结构清晰我们可以把这个字段放在比较合理的位置比如放在stage适用阶段字段后面。最终的SQL语句是这样的ALTER TABLE zt_case ADD COLUMN priority VARCHAR(30) NOT NULL DEFAULT medium COMMENT 用例优先级 AFTER stage;执行这条SQL字段就加好了。AFTERstage 是指定字段位置不加的话默认会放在最后。COMMENT是注释方便以后自己看。3.2 安全须知备份备份备份操作生产环境数据库前一定要备份zt_case表。可以用mysqldump工具或者管理工具的导出功能。测试环境先行务必在测试环境验证无误后再对生产环境进行操作。理解默认值NOT NULL DEFAULT medium保证了即使老数据或某些操作未填写优先级也会有一个合理的默认值避免出现空值导致页面显示问题。4. 核心实战让“优先级”活起来数据库准备好了现在开始写代码。我们会按照数据流动的方向来先从用户看到的页面View开始然后处理语言和选项Lang再实现后台逻辑Control和Model。4.1 扩展视图在页面上“画”出字段视图决定了用户看到什么。我们要在“新建用例”和“编辑用例”的表单里加入一个“优先级”下拉框。方法一覆盖扩展简单直接覆盖扩展顾名思义就是完全替换掉原来的视图文件。适用于对页面改动较大或者原视图逻辑比较简单的情况。在/module/testcase/ext/view/目录下创建一个文件名字必须和原视图文件一致create.html.php。把原文件/module/testcase/view/create.html.php的全部内容复制过来。在我们想插入“优先级”字段的地方比如在“适用阶段”字段所在的行找到对应的HTML代码。通过阅读原代码我们发现它在一个td标签内里面用table-row和table-col做布局。在合适的位置比如“适用阶段”的div后面加入我们自己的代码!-- 原代码片段用于定位 -- div classtable-col idstageBox ... /div !-- 我们新增的优先级字段 -- div classtable-col idpriorityBox div classinput-group span classinput-group-addon fix-border?php echo $lang-testcase-priority;?/span ?php echo html::select(priority, $lang-testcase-priorityList, $priority, classform-control chosen);? /div /div你看代码结构和旁边的“适用阶段”几乎一模一样。这里用到了三个变量$lang-testcase-priority显示“优先级”这个标签。$lang-testcase-priorityList一个数组定义了下拉框的选项‘high’ ‘高’ ‘medium’ ‘中’ ‘low’ ‘低’。$priority这个字段当前的值用于在编辑时回显。新建时这个值由后面的Controller提供。方法二钩子扩展推荐更优雅覆盖扩展要复制整个文件如果原文件很大或者以后禅道升级视图有变动我们合并起来会比较麻烦。更优雅的方式是使用钩子扩展。钩子扩展允许我们只注入一段代码到原视图的特定位置通常是末尾然后用JavaScript把这段HTML“搬运”到我们想要的位置。在/module/testcase/ext/view/目录下创建一个钩子文件。命名规则是方法名.扩展名.html.hook.php。我们命名为create.priority.html.hook.php。在这个文件里我们编写要注入的HTML和JS代码?php // 1. 生成“优先级”字段的HTML代码 $html div class\table-col\ id\priorityBox\; $html . div class\input-group\; $html . span class\input-group-addon fix-border\ . $lang-testcase-priority . /span; $html . html::select(priority, (array)$lang-testcase-priorityList, isset($case-priority) ? $case-priority : $priority, classform-control chosen); $html . /div; $html . /div; ? script // 2. 页面加载后将生成的HTML插入到“适用阶段”字段的后面 $(document).ready(function() { // 找到“适用阶段”字段的容器 var $stageBox $(#stageBox); if ($stageBox.length) { // 将我们的优先级字段HTML插入到其后 $stageBox.after(?php echo json_encode($html); ?); // 如果需要可以重新初始化一下chosen下拉框插件 $(.chosen).trigger(chosen:updated); } }); /script钩子扩展的好处是非侵入式。我们不需要关心原视图文件的具体结构只需要用JS操作DOM。即使未来禅道升级改了create.html.php的布局只要#stageBox这个ID还在我们的代码大概率还能正常工作。对于编辑页面edit方法操作完全类似在/module/testcase/ext/view/下创建edit.priority.html.hook.php即可注意编辑页面通常直接使用$case对象来获取当前值。4.2 扩展语言包让字段“说人话”页面里显示的“优先级”、“高”、“中”、“低”这些文本不应该硬编码在HTML里而应该通过语言包管理方便后续国际化。在/module/testcase/ext/lang/目录下创建对应语言目录比如简体中文是zh-cn。如果目录已存在则跳过。在zh-cn目录下创建一个PHP文件名字可以任意比如priority.php。在这个文件里定义我们的语言变量?php // 定义字段标签 $lang-testcase-priority 优先级; // 定义下拉选项列表 $lang-testcase-priorityList array(); $lang-testcase-priorityList[high] 高; $lang-testcase-priorityList[medium] 中; $lang-testcase-priorityList[low] 低; // 你也可以定义一些帮助信息 $lang-testcase-priorityHelp 用于标识用例执行的紧急程度高优先级用例将优先安排测试。; ?禅道在运行时会自动加载ext/lang/下的所有语言文件并将其合并到核心语言数组中。这样我们在视图文件里用的$lang-testcase-priority就能正确显示了。4.3 扩展控制器做数据的“搬运工”控制器Control是连接视图和模型的桥梁。在“新建用例”页面我们需要给视图传递两个东西字段的当前值$priority和选项列表这个已经在语言包了。在保存表单时控制器需要接收$_POST数据里的priority字段并传递给模型进行保存。扩展 create 方法在/module/testcase/ext/control/目录下创建一个文件文件名必须小写且与方法名一致create.php。文件内容结构如下?php // 引入父类控制文件 include ../../control.php; // 声明一个扩展类类名以 ‘ext’ 开头或以其他方式避免冲突这里用 myTestcase class myTestcase extends testcase { /** * 扩展新建用例方法。 * 注意参数列表必须与父类方法完全一致。 */ public function create($productID, $branch , $moduleID 0, $from , $param 0, $storyID 0, $extras ) { // 1. 设置默认优先级。这个值会传递给视图作为下拉框的默认选中项。 $priority medium; // 默认值为“中” $this-view-priority $priority; // 2. 调用父类的create方法执行原有的所有逻辑如获取产品、模块列表等 parent::create($productID, $branch, $moduleID, $from, $param, $storyID, $extras); } } ?扩展 edit 方法用于编辑页面回显数据同样在/module/testcase/ext/control/目录下创建edit.php。关键点在于编辑页面需要从数据库读取已有的用例信息$case我们的优先级字段值$case-priority已经由父类的edit方法从数据库查出来了。我们通常不需要额外操作因为视图钩子可以直接使用$case-priority。但如果我们需要在控制器层做一些额外的逻辑可以这样写?php include ../../control.php; class myTestcase extends testcase { public function edit($caseID, $from , $extra ) { // 先调用父类方法获取$case等数据 parent::edit($caseID, $from, $extra); // 父类方法执行后$this-view-case 已经存在 // 这里可以针对 $this-view-case-priority 做一些处理比如验证等。 // 通常不需要额外赋值视图层可直接使用。 } } ?关键技巧parent::create(...)这行代码至关重要。它调用了禅道原有的create方法完成了所有基础工作比如分配产品列表、模块树等数据到视图。我们的扩展是在此基础上“打补丁”只添加我们需要的部分。这种方式最大程度地减少了我们对原有逻辑的干扰也最安全。4.4 扩展模型与数据库“对话”模型Model负责最核心的数据存取。当用户点击保存时控制器会把数据交给模型的create或update方法。我们需要确保这些方法能处理我们的priority字段。钩子扩展最常用、最安全的方式我们并不想重写整个create方法只想在它执行前后或者在其中插入对我们字段的处理。钩子扩展完美符合这个场景。在/module/testcase/ext/model/hook/目录下创建钩子文件。命名规则是方法名.扩展名.php。我们为create方法创建create.priority.php。在这个文件里我们可以直接操作数据。禅道框架会在执行核心的create方法之前将钩子文件中的代码合并进去。?php // 文件/module/testcase/ext/model/hook/create.priority.php // 这段代码会被插入到 model 的 create 方法内部。 // 在父类方法执行前我们可以对传入的 $data 进行处理 // 但更常见的做法是框架已经帮我们处理好了 $_POST 到对象属性的绑定。 // 我们这里假设 $this-post-priority 已经存在。 // 其实对于简单的字段添加我们甚至不需要写任何代码 // 因为禅道的 common 模型基类中的 create 方法会智能地将 $this-post 中的所有数据 // 与数据表字段进行匹配并插入。只要我们的字段名 ‘priority’ 在表 zt_case 中存在就会被自动保存。 // 但是如果我们想设置默认值或者进行一些验证可以在这里写 if(empty($this-post-priority)) { // 如果前端没有传递 priority设置一个默认值 $this-post-priority medium; } // 或者我们可以记录日志调试用 // echo 正在创建用例优先级为 . $this-post-priority . \n; ?重要提示对于update方法编辑保存操作完全一样。在/module/testcase/ext/model/hook/下创建update.priority.php文件即可。禅道的通用处理机制使得对于简单的增删改查字段我们经常只需要添加钩子文件内容甚至可以为空框架就能自动完成数据的保存和更新这非常强大。5. 完善与优化让功能更完整基础功能跑通了但一个完整的字段还需要在更多地方亮相并且可能需要一些前端润色。5.1 在浏览列表页展示优先级用户肯定想在用例列表里一眼就看到优先级。这就需要扩展browse方法的视图。定位列表列打开/module/testcase/view/browse.html.php找到显示表格的thead和tbody部分。研究一下“标题”、“模块”这些列是怎么生成的。使用钩子扩展同样我们采用非侵入式的钩子。在/module/testcase/ext/view/下创建browse.priority.html.hook.php。注入列我们需要在表头thead和每一行数据tbody里都加入“优先级”列。这通常需要用到JavaScript来动态修改DOM因为列表的数据是PHP循环生成的直接修改HTML结构比较麻烦。但禅道的列表通常有固定的结构我们可以用jQuery来操作。?php // 文件/module/testcase/ext/view/browse.priority.html.hook.php ? script $(document).ready(function() { // 1. 在表头thead中插入“优先级”列 // 假设“类型”列的索引是我们想插入的位置之后 var $typeHeader $(th[data-idtype]); // 找到“类型”列的表头 if($typeHeader.length) { var $newHeader $(th classw-80px>// 文件/module/testcase/ext/js/create/priority.js $(function(){ // 当优先级选择变化时给个提示 $(#priority).change(function(){ var priority $(this).val(); console.log(优先级变更为 priority); // 这里可以添加更多逻辑比如高亮显示表单某部分 }); });CSS扩展如果想给高优先级的用例在列表里加个红色背景。在/module/testcase/ext/css/目录下创建browse/子目录。创建priority.css文件。添加样式/* 文件/module/testcase/ext/css/browse/priority.css */ tr[data-priorityhigh] { background-color: #fff2f2; /* 浅红色背景 */ font-weight: bold; }然后你需要在前面的JS或PHP中给对应优先级的行加上data-priorityhigh的属性。5.3 别忘了配置扩展为了让禅道识别并启用我们的扩展有时需要在模块的ext/config/目录下放置一个配置文件。不过对于我们这种在标准ext子目录control/,view/等下放置文件的方式禅道16.4的版本通常能自动扫描加载。为了更规范你可以在/module/testcase/ext/config/下创建一个priority.php文件里面进行一些扩展的元数据声明虽然对于简单字段扩展不是必须的。?php // 文件/module/testcase/ext/config/priority.php $config-testcase-customFields[priority] priority; // 声明一个自定义字段 ?6. 调试与排查开发中的“火眼金睛”代码写完了刷新页面却没看到字段或者保存不了数据别慌调试是开发的必修课。检查文件路径和命名这是最常出错的地方。确保所有文件都放在正确的ext子目录下并且文件名大小写、后缀完全正确如create.php不是Create.php。清除缓存禅道有运行时缓存和Smarty模板缓存。修改了代码后务必到后台-系统-缓存点击“清空缓存”。有时候甚至需要手动删除tmp/cache/和tmp/template/下的文件。查看错误日志打开禅道的log/目录查看php.xxx.log和sql.xxx.log。PHP错误日志会告诉你语法错误或未定义变量SQL日志能让你确认你的保存操作是否生成了正确的SQL语句。浏览器开发者工具按F12打开。在“网络(Network)”标签页查看页面加载的JS/CSS文件是否包含你的扩展文件。在“控制台(Console)”查看JS错误。在“元素(Elements)”里检查你的HTML是否被正确插入到DOM中。打印调试法在怀疑的PHP代码位置使用echo或var_dump输出变量值或者用die()中断执行看看程序是否运行到这里。记得调试完要删除这些语句。从简单开始验证先确保语言包生效看页面标签是否变成“优先级”再确保视图钩子生效看HTML是否插入最后验证数据流保存后查数据库。一步步隔离问题。我踩过的一个坑是有一次钩子JS文件没生效排查了半天发现是ext/js/create/目录下文件命名不对或者JS语法错误导致整个文件没加载。所以细心和耐心是关键。7. 扩展你的思路不止于一个字段通过这个完整的“优先级”字段开发流程我们实际上掌握了一套为禅道任何模块添加自定义字段的“组合拳”。这套方法可以复用到很多地方给需求Story加“业务价值”评分在产品经理创建需求时增加一个评分字段。给任务Task加“预估工时”和“实际工时”更精细的工时管理。给Bug加“重现概率”测试人员提交Bug时选择是“必现”还是“偶发”。构建全新的简单模块利用这套扩展机制你甚至可以在module目录下创建一个全新的模块目录然后通过禅道的路由配置将其接入系统。这需要更深入的理解但底层逻辑是相通的。禅道的扩展机制就像一套乐高积木。核心框架提供了稳定的底座和标准的接口ext目录我们这些开发者则用一块块积木control扩展、model钩子、view钩子等去搭建自己需要的功能。它既保证了核心的稳定又给予了我们足够的灵活度。最后再强调一下安全区概念。所有操作都在ext目录下完成这是与官方代码和平共处的基石。多看看禅道官方文档和源码中其他模块的ext示例你会学到更多巧妙的用法。开发完成后记得在不同浏览器、不同场景下充分测试确保你的自定义字段在各种操作流程下新建、编辑、复制、导入导出等都能正常工作。