用Wordpress构建App更新和反馈平台(下)

用户反馈

上文说了,APP的用户反馈功能麻烦在于GooglePlay在国内没法用,但这只是一方面,另一方面用户也不一定乐意在发现问题时再去打开GooglePlay去找到你的应用再评论,能在应用里直接反馈当然是最好了。

当然这个功能做一个也不是很麻烦的事情,无非是在系统后端加一个表,API上加一个函数的事情,但是如果每个应用都要前后端这么做一回还是很烦的,最好还是能有一个统一的渠道。

于 是我想到利用Wordpress本身的评论功能,用户在应用中输入的反馈内容都自动变成相应版本更新文章下面的评论。但是这样似乎就要修改 Wordpress了——所以我一开始并不打算用Wordpress,而是想找个简单点的BLOG程序自己改改,但是试过几个都不满意,最后还是回到 Wordpress上,开始研究它的插件机制。

话说WP的开发文档还是很坑的,特别是插件相关的部分,很多action和filter都只有一名字,没有任何说明,只能自己去看WP的源码,还好源码风格还不错,很简单易懂,于是写了这么一个插件。

Auth Comment插件

<?php
/*
Plugin Name: Auth Comment
Plugin URI: http://mental.we8log.com/
Description: 通过第三方验证后登录发表评论的插件.
Author: raptor<raptor.zh@gmail.com>
Version: 1.0
Author URI: http://mental.we8log.com/
*/

define('AUTH_COMMENT_API_URL', 'http://yoursite.com/api/&#39;); define('AUTH_COMMENT_LOGIN_PATH', '/login'); define('AUTH_COMMENT_LOGOUT_PATH', '/logout'); define('AUTH_COMMENT_GET_KEY', 'get_comment_key'); define('AUTH_COMMENT_GET_PROFILE', 'commenter_profile/');

function api_process($func) { $ch = curl_init(AUTH_COMMENT_API_URL . $func); curl_setopt($ch, CURLOPT_VERBOSE, 0); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); // curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); if (isset($_SERVER['HTTP_COOKIE'])) { curl_setopt($ch, CURLOPT_COOKIE, $_SERVER['HTTP_COOKIE']); } $resp = curl_exec($ch); $info = curl_getinfo($ch); curl_close($ch); if ($info['http_code'] == 200) { return $resp; } else { return NULL; } }

function get_comment_profile($comment_key) { $resp = api_process(AUTH_COMMENT_GET_PROFILE . $comment_key); $user = isset($resp) ? json_decode($resp) : NULL; return $user; }

function ac_comment_form_default_fields($fields) { if (!is_user_logged_in()) { $comment_key = api_process(AUTH_COMMENT_GET_KEY); if (isset($comment_key)) { return array('author' => '<input id="author" name="author" type="hidden" value="' . esc_attr( $comment_key ) . '"' . $aria_req . ' />'); } else { return array(); } } else { return $fields; } }

add_filter( 'comment_form_default_fields', 'ac_comment_form_default_fields' );

function parse_comment_key($defaults) { $author = isset($defaults['fields']) ? ( isset($defaults['fields']['author']) ? $defaults['fields']['author'] : NULL) : NULL; $matches = preg_match("/^<input id=&quot;author&quot;.value=&quot;([0-9a-f])&quot; \/>/i", $author, $matches) ? $matches : NULL; return isset($matches) ? $matches[1] : NULL; }

function ac_comment_form_defaults($defaults) { if (!is_user_logged_in()) { $comment_key = parse_comment_key($defaults); $user = isset($comment_key) ? get_comment_profile($comment_key) : NULL; if (isset($user)) { $defaults['comment_notes_before'] = '<p class="logged-in-as">以 ' . $user->nickname . ' 的身份登录。<a href="' . AUTH_COMMENT_LOGOUT_PATH . '" title="登出账户">登出</a>?</p>'; } else { $defaults['comment_notes_before'] = '<p class="logged-in-as">必须先 <a href="' . AUTH_COMMENT_LOGIN_PATH . '" title="登录账户">登录</a> 才能发表评论。</p>'; $defaults['comment_notes_after'] = ''; $defaults['comment_field'] = ''; $defaults['class_submit'] = 'submit_hidden'; } } return $defaults; }

add_filter( 'comment_form_defaults', 'ac_comment_form_defaults' );

function ac_preprocess_comment($commentdata) { if (!is_user_logged_in()) { $comment_key = (isset($commentdata['comment_author']) && $commentdata['comment_author'] != "") ? $commentdata['comment_author'] : NULL; $user = isset($comment_key) ? get_comment_profile($comment_key) : NULL; if (isset($user)) { $commentdata['comment_author'] = $user->nickname; $commentdata['comment_author_email'] = $user->email; //$commentdata['comment_author_url'] = "/user/" . $user->id; } else { $commentdata = NULL; } } return $commentdata; }

add_filter( 'preprocess_comment', 'ac_preprocess_comment' );

function ac_head_css() { echo " <style type='text/css'> .submit_hidden { display: none; } </style> "; }

add_action( 'wp_head', 'ac_head_css' ); ?>

安装使用方法

修改前面几个选项为你自己应用的相关信息:

1、API_URL。应用后端的API链接, 需要提供两个API方法:一个是get_comment_key,为了安全起见,不建议直接使用应用的OAuth2.0的token,用 OAuth1.0a当然可以,但是就要复杂很多,所以特别增加了这个comment_key只用于提交评论,并且这个key应该设置一个较短的过期时间。 另一个是get_commenter_profile,用于通过comment_key获取用户的相关信息,包括ID、昵称、email,目前实际上只用 了昵称和email两项。

2、登录路径和注销路径。用于与应用的web端整合,如果用户在web端登录,也可以直接在这个WP BLOG上评论。为此,这个BLOG必须置于应用web端的子目录下(这样才能够传递cookie)。另外,如果需要允许用户通过web端评论,API端 的get_comment_key方法也必须能够接受web方式的验证(如cookie或session),当然应用端使用的OAuth 1.0a/2.0也必须同时支持——或者用两个方法分别处理。

3、get_comment_key和get_commenter_profile两个方法在API中的路径。

修改完成后保存为auth-comment.php文件,上传到BLOG的wp-content/plugins目录下,然后在WP管理后台就可以看到这个插件,启用之即可把BLOG的评论功能改造为用户反馈平台。

注意事项:

不要再安装并启用Akismet之类的反垃圾评论插件,会有冲突。因为这个插件限制了只有应用的注册用户可以评论,所以也不会有别的垃圾评论(在插件中把所有非应用注册用户的评论都忽略,甚至都不会进入垃圾评论队列)。

同样,WP选项中的评论审核相关选项都可以关闭——除非你希望用户反馈不公开显示。

插件原理

应 用在提交反馈前,通过OAuth 1.0a/2.0调用后端API的get_comment_key方法,取得comment_key,然后调用WP的post_comment方法,把 comment_key传递给WP,插件在评论被提交到数据库之前截取评论数据,调用get_commenter_profile读取用户相关信息,如果 读取失败则丢弃这条评论,否则用取得的用户信息更新评论数据并提交。

在WP文章显示页面对评论的处理是:如果当前有WP用户登录,则以WP 用户身份显示评论功能;如果未以WP用户身份登录,则把cookie传递给应用的WEB端以获取comment_key,如果用户已登录应用的WEB端, 则用取得的comment_key调用get_commenter_profile以取得用户信息,并以此用户身份显示评论功能;如果也未登录应用的 WEB端,则无法取得comment_key或comment_key无效,隐藏评论功能并提示登录。

客户端实现

在APP端的实现是这样的:

public class FeedbackDialog extends DialogFragment {

public class AsyncFeedbackTask implements AsyncRestCall.AsyncRestCallListener {

    private ProgressFragment mProgress;
    private AsyncRestCall mApi;
    private String mFeedback;

    @Override
    public void onSuccess(Object result) {
        String comment_key = (String)result;
        if (!TextUtils.isEmpty(comment_key)) {
            mApi = new AsyncRestCall(null, OFFICIAL_BLOG_URL,
                    new AsyncRestCall.AsyncRestCallListener() {
                @Override
                public void onSuccess(Object result) {
                    onErrorOrCancel(null);
                }

                @Override
                public void onErrorOrCancel(Throwable e) {
                    if (null != e) {
                        ToastUtils.show(mContext, e.getMessage(), Toast.LENGTH_SHORT);
                    }
                    dismiss();
                    if (null != mProgress) {
                        mProgress.dismiss();
                    }
                }
            },
                    null);
            RestParams params = new RestParams();
            params.add(&quot;author&quot;, comment_key);
            params.add(&quot;comment&quot;, mFeedback);
            params.add(&quot;comment_post_ID&quot;, FEEDBACK_POST_ID);
            params.add(&quot;comment_parent&quot;, &quot;0&quot;);
            params.add(&quot;submit&quot;, &quot;submit&quot;);
            mApi.post(FUNC_FEEDBACK, params, null);
        }
    }

    @Override
    public void onErrorOrCancel(Throwable e) {
        if (null != e) {
            ToastUtils.show(mContext, e.getMessage(), Toast.LENGTH_SHORT);
        }
        dismiss();
        if (null != mProgress) {
            mProgress.dismiss();
        }
    }

    public void postFeedback(String feedback) {
        mFeedback = feedback;
        mProgress = ProgressFragment.newInstance(getString(R.string.progress_feedback));  // show a progress dialog
        mProgress.show(getFragmentManager(), &quot;&quot;);
        mApi = getAPI(mContext, this);  // your backend API
        mApi.get(FUNC_GET_COMMENT_KEY, null, null);  //  Async call, return a string
    }
}

private Context mContext;
private EditText mFeedbackText;
private AsyncFeedbackTask mTask;

public static FeedbackDialog newInstance(Context context) {
    FeedbackDialog dialog = new FeedbackDialog();
    dialog.mContext = context;
    return dialog;
}

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    mFeedbackText = new EditText(mContext);
    mFeedbackText.setLines(4);
    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
    builder.setView(mFeedbackText)
            .setTitle(getString(R.string.title_feedback))
            .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int id) {
                    mTask = new AsyncFeedbackTask();
                    mTask.postFeedback(mFeedbackText.getText().toString());
                }
            })
            .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int id) {
                    dismiss();
                }
            });
    return builder.create();
}

}

这是一个用户提交反馈的对话框,用的是简单的AlterDialog生成,用户点击确定提交时,先进行一次异步的应用后端API调用以取得comment_key,见AsyncFeedbackTask中的postFeedback。

ProgressFragment 是我自己实现的一个显示进度的fragment,此处从略。getAPI返回的是我的后端API异步调用对象,调用其get方法以取得 comment_key,在它的回调成功事件onSuccess中判断是否已经取得comment_key。

如已取得有效的comment_key,则创建一个异步网络调用对象(我自己实现的AsyncRestCall)。OFFICIAL_BLOG_URL为WP BLOG的地址,如:"http://yoursite.com/blog";然后是五个参数:

author:传入comment_key,供Auth Comment插件处理。
comment:用户反馈内容。
comment_post_ID: 这个参数很重要,必须是此APP对应版本的文章ID。那么这样就会有一个先有鸡还是先有蛋的问题了:应用还没发布当然没有更新文章,没有更新文章就没有这 个ID,没有这个ID,这个应用怎么编译发布呢?所以,方法就是先新建一篇文章,但暂不发布,记下其ID写到应用里,编译发布后再把更新文章发布出来。
comment_parent:这个参数始终为0。
submit:这个参数可以为任意字符串,不过还是建议设置为"submit"。

然 后向WP的这个FUNC_FEEDBACK路径发送POST请求,FUNC_FEEDBACK默认为:"/wp-comments-post.php"。 至于请求的返回值就不去处理了,正常情况下应该是返回一个302重定向的。不论成功还是失败,都是简单地把进度框和反馈框都关闭。

现在,用户只要在这个应用里的这个反馈对话框里提交内容,就会自动在BLOG的相应的文章下面出现一条以这个用户身份作出的评论,你可以在BLOG后台统一查看并处理。

推送到[go4pro.org]