• OTRS 二次开发笔记


    公司使用otrs系统处理业务工单,各种事件流。因为是开源免费系统,因此需要在上面做一些功能补充或定制的二次开发。

    otrs是什么?###

    OTRS 是一个功能强大的工单系统。完美适用于服务台(Help Desk)、IT服务管理、流程管理、机会管理和更多其他领域
    它的文档挺详细的:http://doc.otrs.com/doc/

    • 使用者看 Admin Manual
    • 开发者看 Developer Manual 和 API Reference

    otrs用什么开发的?#

    1. 语言 Perl##

    为了开发这个花了一个月时间啃完《Perl入门》《Perl进阶》。因为PHP的关系,某些语法很容易接受,然而在另一些地方总是觉得很蛋疼。。最让我无法接受的是Perl的调试,我到现在还不知道除了看apache错误日志外怎么更好地调试perl的web应用。php因为是脚本语言,不停地echo,print_r然后打开浏览器就可以看到结果。然而perl只能看到一个500 Internal server error. (╯‵□′)╯︵┻━┻

    2. 数据库 Mysql##

    otrs支持多个数据库,一般mysql就好了,没啥好讲的

    3. 安装和环境##

    otrs有rpm包傻瓜式安装,http://ftp.otrs.org/pub/otrs/RPMS/
    otrs的源码安装的话解压就可以了。然后自己配置apache安装perl模块。配置httpd.conf 修改cgi-bin 目录为 otrs的cgi-bin目录。这个配置过程不是很懂,感觉是歪打正着成功的。。。
    打开浏览器 /otrs/installer.pl 按步骤配置好数据库和用户就可以用了。
    otrs 有一个定时任务脚本,需要随时同步更新配置文件,收发邮件等等。因此安装好了之后不要忘了启动它。

    如何补充otrs的功能?#

    1. otrs核心目录 Kernel##

    -- Config 配置目录,包括菜单,访问权限的配置
    -- Modules 模块目录,相当于控制器
    -- Output 模板目录,otrs有一套模板引擎
    -- System 系统目录,各种核心功能驱动器,和数据库操作模型

    弄清楚目录结构当然是首要的。通过学习开发文档和查看源码可以迅速掌握各模块的功能,加载方法。

    2. otrs二次开发##

    目前运行的是otrs4.0版本,我做开发测试的是5.0版本。开发上基本没有差别,只有极个别文件目录不一样。

    配置文件###

    首先我们开发的核心模块都根据各自角色放在相应的文件夹,然后全部放在Custom目录下,如图:

    然后在 Kernel/Config 目录下放置我们的配置文件,主要是添加系统菜单。具体规则可以参考它原有的配置文件来写,下面是我写的一个例子:

    <ConfigItem Name="Frontend::Module###AgentWSCustom" Required="1" Valid="1">
        <Description Translatable="1">FrontendModuleRegistration for WSCustom module.</Description>
        <Group>WSCustom</Group>
        <SubGroup>Frontend::Agent::ModuleRegistration</SubGroup>
        <Setting>
            <FrontendModuleReg>
                <Title>自定义模块</Title>
                <Group>users</Group>
                <Description>自定义模块</Description>
                <NavBarName>自定义</NavBarName>
                <NavBar>
                    <Description Translatable="1">自定义首页</Description>
                    <Name Translatable="1">工单查询</Name>
                    <Link>Action=AgentWSCustom</Link>
                    <LinkOption></LinkOption>
                    <NavBar>自定义</NavBar>
                    <Type></Type>
                    <Block></Block>
                    <AccessKey>w</AccessKey>
                    <Prio>100</Prio>
                </NavBar>
                <NavBar>
                    <Description Translatable="1">自定义菜单</Description>
                    <Type>Menu</Type>
                    <Block>ItemArea</Block>
                    <Name Translatable="1">自定义</Name>
                    <Link>Action=AgentWSCustom</Link>
                    <LinkOption>data-id="10086"</LinkOption>
                    <NavBar>自定义</NavBar>
                    <AccessKey>t</AccessKey>
                    <Prio>10100</Prio>
                </NavBar>
                <Loader>
                    <CSS>Custom.Agent.WSCustom.css</CSS>
                    <JavaScript>Custom.Agent.WSCustom.js</JavaScript>
                </Loader>
            </FrontendModuleReg>
        </Setting>
    </ConfigItem>
    

    配置的菜单链接是 Action=AgentWSCustom, 因此需要在 Custom/Kernel/Modules/ 下建一个 AgentWSCustom.pm 模块(类):

    控制器<AgentWSCustom.pm>##

    package Kernel::Modules::AgentWSCustom;
    
    use strict;
    use warnings;
    
    use POSIX;
    use Encode;
    
    use Kernel::Language qw(Translatable);
    
    # Frontend modules are not handled by the ObjectManager.
    our $ObjectManagerDisabled = 1;
    
    sub new {
        my ( $Type, %Param ) = @_;
    
        # allocate new hash for object
        my $Self = {%Param};
        bless ($Self, $Type);
    
        return $Self;
    }
    
    sub Run {
        my ( $Self, %Param ) = @_;
        my %Data = ();
        
        # store last queue screen
        $Kernel::OM->Get('Kernel::System::AuthSession')->UpdateSessionID(
            SessionID => $Self->{SessionID},
            Key       => 'LastScreenOverview',
            Value     => $Self->{RequestedURL},
        );
        # Kernel modules
        my $LanguageObject = $Kernel::OM->Get('Kernel::Language');
        my $LayoutObject     = $Kernel::OM->Get('Kernel::Output::HTML::Layout');
        my $ParamObject  = $Kernel::OM->Get('Kernel::System::Web::Request');
        my $TimeObject = $Kernel::OM->Get('Kernel::System::Time');
        # Custom module
        my $WSCustom = $Kernel::OM->Get('Kernel::System::WSCustom');
        # do something
        my $Tickets= $WSCustom->GetTicketsList();
        
        # result overview
        $LayoutObject->Block(
            Name => 'OverviewResult',
            Data => {},
        );
        
        for my $t (@{$Tickets}) {
            # output the result data
            $LayoutObject->Block(
                Name => 'OverviewResultRow',
                Data => {
                    %{$t},
                },
            );
        }
        # build output
        my $Output = $LayoutObject->Header();
        $Output   .= $LayoutObject->NavigationBar();
        $Data{hello} = "Hello Xxq, Bye!";
        $Output   .= $LayoutObject->Output(
            Data    => \%Data,
            TemplateFile => 'AgentWSCustom',
        );
        $Output   .= $LayoutObject->Footer();
        return $Output;
    }
    

    控制器的核心是 Run() ,基本结构是先加载各种系统组件,然后操作数据库模型,在这里我们是 my $Tickets= $WSCustom->GetTicketsList(); 。因此需要在 Custom/Kernel/System/ 添加一个名为 WSCustom.pm 模块文件。
    模板输出是 $LayoutObject 控制, Block() 方法可以渲染一个模板块,一般配合循环输出列表或表格。Output() 方法接收绑定到模板的变量Data,和模板文件名TemplateFile。因此需要在 Custom/Kernel/Output/HTML/Templates/Standard 目录下添加一个名为 AgentWSCustom.tt 的模板文件(根据版本不同,这个路径会不一样)。

    数据库模型<WSCustom.pm>##

    package Kernel::System::WSCustom;
    
    use strict;
    use warnings;
    
    # list your object dependencies (e.g. Kernel::System::DB) here
    our @ObjectDependencies = (
        'Kernel::System::DB',
    );
    
    sub new {
        my ( $Type, %Param ) = @_;
        
        # allocate new hash for object
        my $Self = {};
        bless ($Self, $Type);
        
        return $Self;
    }
    
    sub GetTicketsList {
        my ( $Self, %Param ) = @_;
        my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
        # id,工单号,创建时间,修改(完成)时间,工单状态ID
        my $sql = " SELECT t.id,t.tn,t.create_time,t.change_time,t.ticket_state_id state_id, ";
        # 队列,类型,状态,服务类型
        $sql .= " q.name queue,tt.name type,ts.name state,serv.name service, ";
        # 客户,派发人
        $sql .= " cc.name customer,CONCAT(u.last_name,u.first_name) creater FROM ticket t ";
        $sql .= " LEFT JOIN queue q on t.queue_id=q.id ";
        $sql .= " LEFT JOIN ticket_type tt on t.type_id=tt.id ";
        $sql .= " LEFT JOIN ticket_state ts ON t.ticket_state_id=ts.id ";
        $sql .= " LEFT JOIN service serv ON t.service_id=serv.id ";
        $sql .= " LEFT JOIN customer_company cc ON t.customer_id=cc.customer_id";
        $sql .= " LEFT JOIN users u ON t.create_by=u.id ";
        $sql .= " WHERE t.create_time_unix BETWEEN ? AND ? ORDER BY t.id ASC ";
        $DBObject->Prepare(
            SQL => $sql,
            Bind => [ $Param{from}, $Param{to} ]
        );
        my @res;
        while (my @Row = $DBObject->FetchrowArray()) {
            my %row;
            $row{id} = $Row[0];
            $row{tn} = $Row[1];
            $row{create} = $Row[2];
            $row{change} = $Row[3];
            $row{state_id} = $Row[4];
            $row{queue} = $Row[5];
            $row{type} = $Row[6];
            $row{state} = $Row[7];
            $row{service} = $Row[8];
            $row{customer} = $Row[9];
            $row{creater} = $Row[10];
            push @res, \%row;
        }
        return @res;
    }
    

    就是一些纯粹的数据读取,没啥好说的。

    模板文件<AgentWSCustom.tt>###

    <div class="custom-container">
    	<div class="">
    	<form action="[% Env("CGIHandle") %]" method="get">
    		<input type="hidden" name="Action" value="[% Env("Action") %]" id="SearchAction"/>
            <input type="hidden" name="Subaction" value="Search"/>
    		<div class="ws-form-control"><label><span class="ws-form-label">开始:</span><input type="text" name="from" value="[% Data.from %]" placeholder="yyyy-mm-dd" /></label></div>
    		<div class="ws-form-control"><label><span class="ws-form-label">结束:</span><input type="text" name="to" value="[% Data.to %]" placeholder="yyyy-mm-dd" /></label></div>
    		<div class="ws-form-control">
    			<button class="ws-form-btn" name="subAction" value="search">查询</button>
    			<button class="ws-form-btn" name="subAction" value="export">导出</button>
    		</div>
    		<div class="ws-form-control ws-tips">Tips:由于工单数量较多,查询比较耗时。日期跨度建议在60天以内。</div>
    	</form>
    	</div>
    	<div class="result">
    [% RenderBlockStart("OverviewResult") %]
            <div class="Header">
                <h2>[% Translate("结果列表") | html %]</h2>
            </div>
    
            <div class="Content">
                <table class="DataTable" summary="工单列表">
                    <thead>
                        <tr>
                            <th>[% Translate("工单号") | html %]</th>
                            <th>[% Translate("队列") | html %]</th>
                            <th>[% Translate("服务种类") | html %]</th>
                            <th>[% Translate("服务类型") | html %]</th>
                            <th>[% Translate("客户") | html %]</th>
                            <th>[% Translate("工单状态") | html %]</th>
                            <th>[% Translate("派发人") | html %]</th>
                            <th>[% Translate("处理人") | html %]</th>
                            <th>[% Translate("创建时间") | html %]</th>
                            <th>[% Translate("关闭时间") | html %]</th>
                            <th>[% Translate("总时长") | html %]</th>
                        </tr>
                    </thead>
                    <tbody>
    [% RenderBlockStart("NoDataFoundMsg") %]
                        <tr>
                            <td colspan="4">
                            [% Translate("No result found.") | html %]
                            </td>
                        </tr>
    [% RenderBlockEnd("NoDataFoundMsg") %]
    [% RenderBlockStart("OverviewResultRow") %]
                        <tr class="">
                            <td>
                            	<a href="[% Env("Baselink") %]Action=AgentTicketZoom;TicketID=[% Data.id | uri %]" title="" class="MasterActionLink">[% Data.tn | html %]</a>
                            </td>
                            <td>[% Data.queue | html %]</td>
                            <td>[% Translate(Data.type) | html %]</td>
                            <td>[% Data.service | html %]</td>
                            <td>[% Data.customer | html %]</td>
                            <td>[% Translate(Data.state) | html %]</td>
                            <td>[% Data.creater | html %]</td>
                            <td>[% Translate(Data.handler) | html %]</td>
                            <td>[% Data.create | html %]</td>
                            <td>[% Data.close | html %]</td>
                            <td>[% Data.totalTime | html %]</td>
                        </tr>
    [% RenderBlockEnd("OverviewResultRow") %]
                    </tbody>
                </table>
            </div>
    [% RenderBlockEnd("OverviewResult") %]
    	</div>
    </div>
    [% WRAPPER JSOnDocumentComplete %]
    <script type="text/javascript">
    $(function(){
    	console.log('It works!!!');
    });
    </script>
    [% END %]
    

    模板里比较重要的是Translate() 方法,另外 JSOnDocumentComplete 支持你直接插入JS代码。

    静态css 和 js

    页面引入的静态css,js 需要在config的xml文件中配置,然后放入目录 var/httpd/htdocs/skins/Agent/default/css/var/httpd/htdocs/js/,如图:

    到这里我的自定义模块大功告成。

    3. 乱码的坑##

    之前尝试在控制器中绑定中文字符串,在页面输出总是乱码。后来无意之中在控制器先对中文字符串解码 Encode.decode("utf8", "中文字符"), 然后完美解决,包括导出到csv,excel文件。

    import Encode;
    $ZhStr = decode("utf8", "中文是必要的");
  • 相关阅读:
    Zepto结合Swiper的选项卡
    Angular选项卡
    创建简单的node服务器
    封装ajax
    JQuery和html+css实现鼠标点击放烟花
    js实现螺旋纹理特效
    Angular入门
    Angular JS例子 ng-repeat遍历输出
    Angular 基础教程(1)
    PHP数组
  • 原文地址:https://www.cnblogs.com/dapianzi/p/7449726.html
Copyright © 2020-2023  润新知