标签 Discuz 下的文章

Discuz! 应用开发中遇到的两个坑

坑一:common()方法并不是所有类都会执行的。

这是Discuz!插件开发文档里的描述:

common() 所有模块执行前被调用 全局嵌入点类

代码如下:

class plugin_test {
    
    function common() {
        echo 123;
    }
}

class plugin_test_forum extends plugin_test {
    
    function viewthread_top_output() {
        echo 234;
    }
}

访问论坛页面,并不会触发到父类plugin_test的common方法执行。

坑二:系统函数updatetable()存在bug。

先看函数定义(代码较多,中间部分省略只做注释):

if(!$query = DB::query("SHOW CREATE TABLE ".DB::table($newtable), 'SILENT')) {
    // 创建表逻辑
} else {
    $value = DB::fetch($query);
    $oldcols = updatetable_getcolumn($value['Create Table']);

    $updates = array();
    $allfileds =array_keys($newcols);
    foreach ($newcols as $key => $value) {
        if($key == 'PRIMARY') {
            // 添加主键
        } elseif ($key == 'KEY') {
            // 添加索引
        } elseif ($key == 'UNIQUE') {
            // 添加索引
        } else {
            // 添加字段
        }
    }

    if(!empty($updates)) {
        $usql = "ALTER TABLE ".DB::table($newtable)." ".implode(', ', $updates);
        if(!DB::query($usql, 'SILENT')) {
            return array(-1, $newtable);
        }
    }
}

通过代码能够看出,这个方法是可以根据表中的结构决定是否需要添加字段。
由于本人需要往post表添加字段,所以需要判断是否已有字段才进行添加。
实际使用中发现,永远不会执行到添加字段的逻辑,if(!$query = DB::query("SHOW CREATE TABLE ".DB::table($newtable), 'SILENT'))这个判断永远成立。

问题的根源在install的sql本身,代码如下:

CREATE TABLE IF NOT EXISTS `pre_forum_post` (
`extstring` varchar(15) NOT NULL,
) ENGINE=MyISAM;

注意上面这里的这个匹配

preg_match_all("/CREATE\s+TABLE.+?pre\_(.+?)\s*\((.+?)\)\s*(ENGINE|TYPE)\s*=\s*(\w+)/is", $sql, $matches);
$newtables = empty($matches[1])?array():$matches[1];

会导致newtables = forum_post`,执行的结果当然是false。
所以sql里不要写`,这里算是考虑不严的一个bug吧。

Discuz! 嵌套回复实现

最近支援一个项目,采用discuz!程序,需要实现嵌套回复,效果类似WordPress的效果,某条回复的回复需要排在一起显示。

嵌套回复效果

没有使用discuz!的点评功能,而是通过改造post表实现嵌套功能。

数据库结构

在discuz!的post表增加字段:

alter table pre_forum_post add parentid int(10) unsigned NOT NULL DEFAULT '0',topid int(10) unsigned NOT NULL DEFAULT '0';

- parentid 父级回复id
- topid 根回复id

添加回复

- 需要传递父级回复pid.
- 如果是根回复,parentid = 0,topid = 0.
- 如果是子回复,parentid = 父级回复的pid,topid = 父级回复的topid. 父级回复的pid和topid需要根据传递的pid进行查询.

查询一个回复下的所有嵌套回复

取一个根回复下的所有子回复,执行sql:

select * from pre_forum_post where toppid = xxx order by qpid desc, pid desc;

php处理成树形结构。

// $rows 为执行上述sql的查询结果
foreach ($rows as $k => $row) {
    if ($row['toppid'] == $row['parentid']) {
        $key = $row['parentid'] . '-' . $row['pid'];
    } else {
        $key = $map[$row['parentid']] . '-'. $row['pid'];
    }
    $map[$k] = $key;
}

ksort($map);
// 排序后的结果:
// 1
// 1-4
// 1-4-7
// 1-5
// 1-2

foreach ($map as $pid) {
   $result[$pid] = $rows[$pid];
}
// result 即为排序好后的回复列表

缓存

- 对所有topid的嵌套回复建立缓存。
pid => array(), array()中存储其下所有嵌套回复。
- 列表页直接查询缓存显示。

缺点

- 更新时触发缓存更新,需要一次取出topid下的所有回复进行php排序处理。
- 回复过多时,缓存过大或无法缓存。

其他

- 因为项目需求特殊,一条回复的嵌套回复只显示前50条,所以缓存不会太大。上面这种方案应该可以满足需求,但是需要考虑查询如何优化。
- 网上搜索了相关的资料,也没有找到太合适的方案。网上的方案是取出数据后进行递归遍历,后续比较下两种方式的效率差别。

参考资料

- 无限分类设计方案 http://www.ccvita.com/315.html
- 在数据库中存储层次数据 http://shiningray.cn/hierarchical-data-database.html

升级Discuz! X3.1后QQ互联提示connect_error_code_0的问题排查

问题表现

升级后,点击QQ互联登录按钮,提示下面的错误信息。

抱歉,当前存在网络问题或服务器繁忙,详细错误:connect_error_code_0,错误代码:,请您稍候再试。

原因分析

Discuz! X3.1的QQ互联集成了OAuth1.0和OAuth2.0的接口,升级脚本判断如果服务器支持ssl,就会使用OAuth2.0接口。
出现此问题的站点属于切换到了OAuth2.0接口后导致的。

source\plugin\manyou\Service\Client\OAuth.php

	public function dfsockopen($requestURL, $queryString = array(), $files = false) {
		return dfsockopen($requestURL, 0, $queryString, '', false, $this->_apiIp, 15, TRUE, !$files ? 'URLENCODE' : 'FORMDATA', true, 0, $files);
	}

注意这里请求url的使用,使用了$this->_apiIp参数,看下这个参数是在哪里定义的。

source\plugin\manyou\Service\Client\ConnectOAuth.php

	public function __construct($connectAppId = '', $connectAppKey = '', $apiIp = '') {

		if(!$connectAppId || !$connectAppKey) {
			global $_G;
			$connectAppId = $_G['setting']['connectappid'];
			$connectAppKey = $_G['setting']['connectappkey'];
		}
		$this->setAppkey($connectAppId, $connectAppKey);
		if(!$this->_appKey || !$this->_appSecret) {
			throw new Exception('connectAppId/connectAppKey Invalid', __LINE__);
		}

		if(!$apiIp) {
			global $_G;
			$apiIp = $_G['setting']['connect_api_ip'] ? $_G['setting']['connect_api_ip'] : '';
		}

		if($apiIp) {
			$this->setApiIp($apiIp);
		}
	}

这里可以看到使用的是后台设置的互联接口IP。

Discuz! 后台诊断工具里的互联接口IP是设置的OAuth1.0接口的域名,即http://openapi.qzone.qq.com。
而OAuth2.0接口的域名变更为了https://graph.qq.com,但是接口里使用的IP仍为OAuth1.0的,所以就导致无法请求,继而导致上述报错。

解决方法

上诊断工具里,去掉设置的互联接口IP即可。

2013年12月17日更新:
看到Discuz! 官网很多人反馈出现网络繁忙的问题,记日志看了下是请求用户openId这步报错了,应该是空间的接口有问题,静待官方解决。

2013年12月18日更新:
已经修复。

[吐槽]Discuz!应用的更新机制

Discuz!应用的更新机制

第一眼看到上面的应用更新提示,你认为点哪里会进行更新。

相信多数人都会选择后面的更新链接,但是当你点击完更新后,会提示你此插件已更新到最新版本。
Discuz!应用的更新机制

返回上一页后,你会发现它还在提示有新版更新。你是不是晕了?明明提示有更新,点更新后却又提示已更新到最新版。

就是这个扯淡的提示,搞的哥把整个插件的更新机制看了一遍,终于搞明白了。
原因就是:

点击那个红色的提示文字进行的是在线更新操作;
点击后面那个更新链接进行的是本地更新操作。

本地更新成功的提示是要先上传新版插件文件到对应的插件目录,在线更新则是直接从应用中心下载新版插件覆盖升级。

吐槽:

话说现在Discuz!应用中心都不支持插件本地下载了,必须从后台进行安装更新。
这明显导致后面那个更新链接没用了,就不能把后面那个改成在线更新啊!!
话说你能不能加个提示啊!!!!

开启Memcache缓存后后台社区QQ群搜索不到帖子的问题排查

问题描述:

此问题发现于Discuz! X2.5版本。
开启了Memcache缓存,后台社区QQ群推送消息页面按tid搜索不到帖子或搜索出来的帖子不对,如图。
开启Memcache缓存后后台社区QQ群按tid搜索帖子出现其他帖子

问题分析:

找到source\admincp\cloud\cloud_qqgroup.php文件,有如下代码:

if($srchtid) {
	$threads = C::t('forum_thread')->fetch_all_by_tid_displayorder($srchtid, 0);
}

这里会调用fetch_all_by_tid_displayorder方法,找到source\class\table\table_forum_thread.php文件,有如下代码:

public function fetch_all_by_tid_displayorder($tids, $displayorder = null, $glue = '>=', $fids = array(), $closed = null) {
    $data = array();
    if(!empty($tids)) {
        $data = $this->fetch_all_by_tid($tids);
        $fids = $fids && !is_array($fids) ? array($fids) : $fids;
        foreach($data as $tid => $value) {
            if($displayorder !== null && !(helper_util::compute($value['displayorder'], $displayorder, $glue))) {
                unset($data[$tid]);
            } elseif(!empty($fids) && !in_array($value['fid'], $fids)) {
                unset($data[$tid]);
            } elseif($closed !== null && $value['closed'] != $closed) {
                unset($data[$tid]);
            }
        }
    }
    return $data;
}

注意这个函数的参数:$tids应该为数组。这里调用了fetch_all_by_tid方法,还是这个文件,有如下代码:

public function fetch_all_by_tid($tids, $start = 0, $limit = 0, $tableid = 0) {
    $data = array();
    if(($data = $this->fetch_cache($tids)) === false || count($tids) != count($data)) {
        if(is_array($data) && !empty($data)) {
            $tids = array_diff($tids, array_keys($data));
        }
        if($data === false) $data = array();
        if(!empty($tids)) {
            $parameter = array($this->get_table_name($tableid), $tids);
            $query = DB::query("SELECT * FROM %t WHERE tid IN(%n)".DB::limit($start, $limit), $parameter);
            while($value = DB::fetch($query)) {
                $data[$value['tid']] = $value;
                $this->store_cache($value['tid'], $value, $this->_cache_ttl);
            }
        }
    }
    return $data;
}

注意这个函数的参数:$tids应该为数组。这里又调用到fetch_cache和store_cache方法。找到source\class\discuz\discuz_table.php文件,里面有这两个方法的定义,

public function fetch_cache($ids, $pre_cache_key = null) {
    $data = false;
    if($this->_allowmem) {
        if($pre_cache_key === null)	$pre_cache_key = $this->_pre_cache_key;
        $data = memory('get', $ids, $pre_cache_key);
    }
    return $data;
}

public function store_cache($id, $data, $cache_ttl = null, $pre_cache_key = null) {
    $ret = false;
    if($this->_allowmem) {
        if($pre_cache_key === null)	$pre_cache_key = $this->_pre_cache_key;
        if($cache_ttl === null)	$cache_ttl = $this->_cache_ttl;
        $ret = memory('set', $id, $data, $cache_ttl, $pre_cache_key);
    }
    return $ret;
}

这两个方法最终调用缓存类的get和set方法,找到source\class\discuz\discuz_memory.php文件,有如下代码:

public function get($key, $prefix = '') {
    static $getmulti = null;
    $ret = false;
    if($this->enable) {
        if(!isset($getmulti)) $getmulti = method_exists($this->memory, 'getMulti');
        $this->userprefix = $prefix;
        if(is_array($key)) {
            if($getmulti) {
                $ret = $this->memory->getMulti($this->_key($key));
                //格式化数组的KEY,去掉表前缀
                if($ret !== false && !empty($ret)) {
                    $_ret = array();
                    foreach((array)$ret as $_key => $value) {
                        $_ret[$this->_trim_key($_key)] = $value;
                    }
                    $ret = $_ret;
                }
            } else {
                $ret = array();
                $_ret = false;
                //循环取值
                foreach($key as $id) {
                    if(($_ret = $this->memory->get($this->_key($id))) !== false && isset($_ret)) {
                        $ret[$id] = $_ret;
                    }
                }
            }
            //无值返回false
            if(empty($ret)) $ret = false;
        } else {
            $ret = $this->memory->get($this->_key($key));
            if(!isset($ret)) $ret = false;
        }
    }
    return $ret;
}

public function set($key, $value, $ttl = 0, $prefix = '') {

    $ret = false;
    if($value === false) $value = '';
    if($this->enable) {
        $this->userprefix = $prefix;
        $ret = $this->memory->set($this->_key($key), $value, $ttl);
    }
    return $ret;
}

注意get方法里,有对$key进行是否为数组的判断,如下代码:

if(is_array($key)) {

这里如果是数组则返回的结果为二维数组,key => value的格式;反之返回的结果是一维数组,只有value。
通过记录log发现source\admincp\cloud\cloud_qqgroup.php文件调用fetch_all_by_tid_displayorder方法的返回结果为一维数组,只有value,而正常来说fetch_all_by_tid_displayorder方法返回的结果应该是二维数组结构。

所以推断是由于source\admincp\cloud\cloud_qqgroup.php文件调用fetch_all_by_tid_displayorder方法时传递的$srchtid参数类型非数组导致的问题,进一步分析发现是由于Discuz! 所有要求参数为数组的地方没有进行是否是数组类型的判断导致。

解决方法:

不给出暂时的解决方法了,静待Discuz! 修复。