当我们设计的站点应用对安全有一定要求的话,就可能会涉及到需要记录使用者的操作记录,比如什么时候登录什么时候做了些什么之类的,你也可以称之为用户操作足迹。

具体来说要实现的最终要得到的数据是如下的:

吴给力在 2012-02-18 01:14:13 上了厕所
吴给力在 2012-02-18 01:17:23 回到床上
吴给力在 2012-02-18 02:26:43 接起电话
吴给力在 2012-02-18 02:27:11 放下电话

就针对上面这个描述来说的话,我们可以有两种解决办法:在吴给力身上装一个记录器,凡是与其身体有接触的东西都会被记录下来同时记录触发的时间;在所有东西上放置记录器,每当触摸人是吴给力时就会连同时间记录下来,理论上两种都是可行的办法,既然这样那就写写试试吧。

1——在吴给力身上装个记录器

每次在吴给力进行动作时都会将该动作记录下来,让我们先写点伪代码梳理一下思路先

吴给力就是一个用户对象,所以我们需要有个用户类,在实例化时初始化用户的ID与名字,有一个声明为public的action方法就是用户做某动作的方法,有一个声明为public的userLog获取用户之前做过的动作记录

于是我们使用的时候根本不需要手动进行记录动作,我们要在action中就定义了有记录动作的功能

$paperen = new User( 1, 'paperen');
$paperen->action( '洗厕所…' );
print_r( $paperen->userLog() );

输出的结果为

Array
(
[0] => Array
(
[time] => 11:31:26
[action] => paperen 洗厕所…
)

)

当然你还可以给构造函数第三个参数,设置是否开启动作记录功能,也就是一个false或者true就行了,user类里面多一个私有变量,下面是paperen写的代码可以参考下

user.php

/**
* 用户类
*/
class User
{

// 用户ID
private $_userId;
// 用户名
private $_userName;
// 动作记录数据
private $_userLogData = array( );
// 是否开启记录器
private $_needLog;

/**
* 构造函数
* @param int $userId 用户唯一ID
* @param string $userName 用户名
* @param bool $needLog 是否开启记录器
*/
function __construct( $userId, $userName, $needLog = true )
{
$this->_userId = intval( $userId );
$this->_userName = $userName;
$this->_needLog = $needLog;
}

/**
* 做某些动作
*/
public function action( $action )
{
//----------------------
/* 做指定的动作 */
//----------------------

if ( $this->_needLog ) $this->_log( $action );
}

/**
* 记录用户动作
* @param string $action 动作
*/
private function _log( $action )
{
$log = array(
'time' => date( 'H:i:s' ),
'action' => $this->_userName . ' ' . $action,
);
$this->_userLogData[] = $log;
}

/**
* 获取所有用户记录
* @return array
*/
public function userLog()
{
return $this->_userLogData;
}

}

index.php

date_default_timezone_set( 'PRC' );

require( 'user.php' );

// 用户ID
$userId = 1;
// 用户名
$userName = 'paperen';

// 实例化一个paperen给我们玩弄
$paperen = new User( $userId, $userName );

// 获取当前的动作
$do = isset( $_GET['do'] ) ? $_GET['do'] : '';
// 合法的动作
$validAction = array(
'wc' => '上厕所',
'sleep' => '睡觉',
'phone' => '听电话',
'hangup' => '挂掉电话',
);
if ( isset( $validAction[$do] ) ) $paperen->action( $validAction[$do] );

// 输出动作
print_r( $paperen->userLog() );

到目前就是有个雏形,其实实际中并不会像这样这么简单或者不会像这个这么理想,而且设计的模式也可能是不一样的,我们的应用有时候动作的触发点并不会放在用户身上,也不一定是单入口,我们可能需要手动记录,ok,没关系如果是需要手动记录的话将_log改为log并声明为public好了,好吧,上面这些似乎远远不足以用于实际的应用,先别急让我们再将第二种方法实现看看

2--在“东西”上安装记录器

在东西上安装记录器,对于第一个方法而言,你可以理解是在动作上安装记录器,而这次我们有必要有个路由了(就是要分析出用户进行的是什么动作请求时附加了什么参数)然后再分发到相应的动作上进行处理。

基于代码有点多,于是这里只放出index.php的代码,关于相关代码,请到这个链接获取http://paperen.com/demo/logger/logger.rar

以下为index.php中的代码

date_default_timezone_set( 'PRC' );

// 引入用户
require( 'user.php' );
// 用户ID
$userId = 1;
// 用户名
$userName = 'paperen';
// 实例化一个paperen给我们玩弄
$paperen = new User( $userId, $userName );

// 引入路由
require( 'router.php' );
$router = new Router();

// 引入记录器
require( 'logger.php' );
$logger = new Logger( $paperen );

// 引入动作器
require( 'action.php' );
// 获取请求的动作
$routerAction = $router->action();
// 动作类
$actionClass = $routerAction . 'Action';
if ( !file_exists( $actionClass . '.php' ) ) exit("{$actionClass}丢失");
require( $actionClass . '.php' );

// 实例化该动作
$action = new $actionClass( $logger );
$action->go( $routerAction );

print_r( $logger->userLog() );

比第一个版本复杂了许多,主要是引入了路由的概念还有将记录器分离开了,至于在你的应用中记录器可能是与数据库打交道的,所以你需要将数据库操作的实例也告诉logger才行,当在访问 http://localhost/test/log/index.php/sleep 这个URL时将会输出以下内容

去睡觉
Array
(
[0] => Array
(
[time] => 15:04:53
[action] => paperen sleep
)

)

到此想必你已经有了你自己的想法,你可以自己动手去试试实现自己的这个动作记录器,甚至你会有更好的想法与更简洁的设计,但请记住不要在未开始设计前就否定了或者看低这些功能,永远不要放弃动手的机会。

你可以选择更随便更易懂但低智能的一些解决方法,你可以称之为过程化、函数化编程方式,但是paperen觉得你应该尝试使用对象的方式去实现这个功能。

// 比如我自己知道这是处理sleep动作的控制器
// 获取用户名(假设你存在session中)
$userName = $_SESSION['username'];
$actionString = "{$userName} 去了睡觉";
// 组织SQL插入logger数据表
$sql = "insert logger...";
$db->query( $sql );

当然你还可以将其封装到一个函数中,但是这样一来你的记录器其实就是一个函数,你要在你想使用的时候调用它,它看上去并不是一个功能块,而是一个辅助函数,也不是说这没有什么好处,但是paperen就是感觉有点不太合适。

还有你最好能避免在每个动作前都要手动加上这么一段代码或者调用一下这个记录器的函数,即使不是什么很严重的问题,但是当你预料到有一天不需要记录器了,你以前加的这些代码将要一个个删掉,这也并不是什么蛋疼的假设,而是作为一个码农你得做得更加智能,不要老是看着目前而忘记了未来。

到这里这篇博文已经完毕,一定要自己尝试下手去实现,在那个过程你会想到更多而且能收获更多。而下面为paperen在CI的框架上扩展的这个功能的一些尝试,这个功能其实是与权限分不开的,可能还要进行一些调整。

需要继承CI的控制器,在application/core/中放下MY_controller.php(当然如果你配置文件中关于自己扩展的文件是为MY的话)

class MY_Controller extends CI_Controller
{
function __construct()
{
parent::__construct();

// 假设用户ID为1
// 其实是要先判断用户是否合法的才会有后续的步骤,但在此略过
$userId = 1;

// 加载权限类
$this->load->library( 'permission' );

// 加载记录器类
$this->load->library( 'userlogger' );

// 当前请求的控制器与方法(假设使用的URL权限机制)
$currentAction = $this->router->fetch_class() . '_' . $this->router->fetch_method();
if ( !$this->permission->verifyPermission( $currentAction ) ) exit('抱歉,没有权限');

// 记录用户行为
$this->userlogger->record( $userId, $this->permission->translatePermission( $currentAction ) );
}
}

增两个类permission与userlogger,都在application/libraries下

permission.php

/*
* 权限类
*/
class Permission
{
/**
* 假设我的权限有这两个
* @var array
*/
private $_permission = array(
'welcome_token' => '欢迎令牌',
'form_index' => '表单提交',
);

/**
* 检查是否有权限
* @param string 请求的动作
*/
public function verifyPermission( $currentAction )
{
return isset( $this->_permission[$currentAction] );
}

/**
* 权限翻译
* @param string $currentAction 权限代码
* @return string
*/
public function translatePermission( $currentAction )
{
return isset( $this->_permission[$currentAction] ) ? $this->_permission[$currentAction] : '';
}
}

userlogger.php

/*
* 用户动作记录器
*
*/
class UserLogger
{
/**
* CI实例
* @var CI
*/
private $_CI;

function __construct()
{
$this->_CI =& get_instance();
}

/**
* 记录用户行为
* @param int $userId 用户ID
* @param string $record 行为
*/
public function record( $userId, $record )
{
$this->_CI->load->model('userlog_model');
$this->_CI->userlog_model->insert( $userId, $record );
}

}

当然你需要为记录器提供数据表的支持,如果你不打算使用数据表记录的话可以自己更改userlogger中record中的记录保存方式,而paperen这个例子中使用的是一个叫userlog模型,对应着数据库中userlog这张表。当访问了http://localhost/codeigniter/index.php/form与http://localhost/codeigniter/index.php/welcome/token 时将触发自动记录用户行为的动作了,你还可以考虑在config中加入是否启动行为记录器的配置

userlog表会有如下数据

20120219164533

这已经比较智能,但是paperen觉得依然有更进一步的地方,而且在实际中可能并不是每个URI都得记录的,就是存在特殊情况。还有上面那个例子paperen是假设此人具有两个权限,welcome_token与form_index,分别对应welcome控制器的token方法与form控制器的index方法,如果你访问的是其他控制器或者welcome控制器的index方法之类的将会提示没有权限。

最后的最后,paperen建议不要将URI与权限混在一起了,进行权限设计的时候也不要将权限绑定到URI上,因为这个不合理,尽管有时候解析得通但是最好避免那样做。

thanks for reading!