WordPress mu-plugin,支持 /t/ /c/ /u/ /p/ /g/ /h/ 六种短链格式 301 重定向到 Discourse 社区,带访问统计和仪表盘小工具 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
300 lines
11 KiB
PHP
300 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* Plugin Name: Discourse Short Links
|
|
* Description: 短链接重定向到文派社区
|
|
* Version: 1.1.0
|
|
* Author: WPCommunity
|
|
*
|
|
* 配置方式 (在 wp-config.php 中添加):
|
|
* define('DISCOURSE_SHORTLINK_URL', 'https://your-discourse.com');
|
|
* define('DISCOURSE_SHORTLINK_STATS', true);
|
|
*
|
|
* 支持格式:
|
|
* /t/123 → 话题
|
|
* /c/slug → 分类
|
|
* /u/user → 用户
|
|
* /p/123 → 帖子
|
|
* /g/group → 群组
|
|
* /h/tag → 标签
|
|
*/
|
|
|
|
// 默认配置(可在 wp-config.php 中覆盖)
|
|
if (!defined('DISCOURSE_SHORTLINK_URL')) {
|
|
define('DISCOURSE_SHORTLINK_URL', 'https://wpcommunity.com');
|
|
}
|
|
if (!defined('DISCOURSE_SHORTLINK_STATS')) {
|
|
define('DISCOURSE_SHORTLINK_STATS', true);
|
|
}
|
|
|
|
/**
|
|
* 记录统计
|
|
*/
|
|
function discourse_shortlink_log($type, $target) {
|
|
if (!DISCOURSE_SHORTLINK_STATS) return;
|
|
|
|
$stats = get_option('discourse_shortlink_stats', []);
|
|
$today = date('Y-m-d');
|
|
|
|
if (!isset($stats[$today])) $stats[$today] = [];
|
|
$key = $type . ':' . $target;
|
|
if (!isset($stats[$today][$key])) $stats[$today][$key] = 0;
|
|
$stats[$today][$key]++;
|
|
|
|
// 保留 30 天
|
|
$cutoff = date('Y-m-d', strtotime('-30 days'));
|
|
foreach (array_keys($stats) as $date) {
|
|
if ($date < $cutoff) unset($stats[$date]);
|
|
}
|
|
|
|
update_option('discourse_shortlink_stats', $stats);
|
|
}
|
|
|
|
/**
|
|
* 获取统计
|
|
*/
|
|
function discourse_shortlink_get_stats() {
|
|
$stats = get_option('discourse_shortlink_stats', []);
|
|
$summary = ['total'=>0, 'today'=>0, 'yesterday'=>0, 'week'=>0, 'by_type'=>[], 'top_links'=>[]];
|
|
|
|
$today = date('Y-m-d');
|
|
$yesterday = date('Y-m-d', strtotime('-1 day'));
|
|
$week_ago = date('Y-m-d', strtotime('-7 days'));
|
|
$all_links = [];
|
|
|
|
foreach ($stats as $date => $links) {
|
|
foreach ($links as $key => $count) {
|
|
$summary['total'] += $count;
|
|
if ($date === $today) $summary['today'] += $count;
|
|
if ($date === $yesterday) $summary['yesterday'] += $count;
|
|
if ($date >= $week_ago) $summary['week'] += $count;
|
|
|
|
$type = explode(':', $key)[0];
|
|
if (!isset($summary['by_type'][$type])) $summary['by_type'][$type] = 0;
|
|
$summary['by_type'][$type] += $count;
|
|
|
|
if (!isset($all_links[$key])) $all_links[$key] = 0;
|
|
$all_links[$key] += $count;
|
|
}
|
|
}
|
|
|
|
arsort($all_links);
|
|
$summary['top_links'] = array_slice($all_links, 0, 10, true);
|
|
return $summary;
|
|
}
|
|
|
|
/**
|
|
* 重置统计
|
|
*/
|
|
function discourse_shortlink_reset_stats() {
|
|
delete_option('discourse_shortlink_stats');
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 重定向处理
|
|
*/
|
|
add_action('init', function() {
|
|
$uri = strtok($_SERVER['REQUEST_URI'], '?');
|
|
$url = DISCOURSE_SHORTLINK_URL;
|
|
$redirect = $type = $target = null;
|
|
|
|
// 话题: /t/123
|
|
if (preg_match('#^/t/(\d+)/?$#', $uri, $m)) {
|
|
$type = 'topic';
|
|
$target = $m[1];
|
|
$redirect = $url . '/t/-/' . $m[1];
|
|
}
|
|
// 分类: /c/slug 或 /c/parent/child
|
|
elseif (preg_match('#^/c/(.+?)/?$#', $uri, $m)) {
|
|
$type = 'category';
|
|
$target = $m[1];
|
|
$redirect = $url . '/c/' . $m[1];
|
|
}
|
|
// 用户: /u/username
|
|
elseif (preg_match('#^/u/([^/]+)/?$#', $uri, $m)) {
|
|
$type = 'user';
|
|
$target = $m[1];
|
|
$redirect = $url . '/u/' . $m[1];
|
|
}
|
|
// 帖子: /p/123
|
|
elseif (preg_match('#^/p/(\d+)/?$#', $uri, $m)) {
|
|
$type = 'post';
|
|
$target = $m[1];
|
|
$redirect = $url . '/p/' . $m[1];
|
|
}
|
|
// 群组: /g/groupname
|
|
elseif (preg_match('#^/g/([^/]+)/?$#', $uri, $m)) {
|
|
$type = 'group';
|
|
$target = $m[1];
|
|
$redirect = $url . '/g/' . $m[1];
|
|
}
|
|
// 标签: /h/tagname
|
|
elseif (preg_match('#^/h/([^/]+)/?$#', $uri, $m)) {
|
|
$type = 'tag';
|
|
$target = $m[1];
|
|
$redirect = $url . '/tag/' . $m[1];
|
|
}
|
|
|
|
if ($redirect) {
|
|
discourse_shortlink_log($type, $target);
|
|
wp_redirect($redirect, 301);
|
|
exit;
|
|
}
|
|
}, 1);
|
|
|
|
/**
|
|
* 处理重置请求
|
|
*/
|
|
add_action('admin_init', function() {
|
|
if (isset($_POST['discourse_shortlink_reset']) && check_admin_referer('discourse_shortlink_reset_nonce')) {
|
|
if (current_user_can('manage_options')) {
|
|
discourse_shortlink_reset_stats();
|
|
add_action('admin_notices', function() {
|
|
echo '<div class="notice notice-success is-dismissible"><p>短链统计已重置。</p></div>';
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 仪表盘小工具
|
|
*/
|
|
add_action('wp_dashboard_setup', function() {
|
|
wp_add_dashboard_widget(
|
|
'discourse_shortlinks_widget',
|
|
'短链统计',
|
|
'discourse_shortlinks_dashboard_widget'
|
|
);
|
|
});
|
|
|
|
function discourse_shortlinks_dashboard_widget() {
|
|
$stats = discourse_shortlink_get_stats();
|
|
$labels = ['topic'=>'话题', 'category'=>'分类', 'user'=>'用户', 'post'=>'帖子', 'group'=>'群组', 'tag'=>'标签'];
|
|
$allowed = array_keys($labels);
|
|
$stats['by_type'] = array_filter($stats['by_type'], fn($v, $k) => in_array($k, $allowed), ARRAY_FILTER_USE_BOTH);
|
|
$stats['top_links'] = array_filter($stats['top_links'], fn($v, $k) => in_array(explode(':', $k)[0], $allowed), ARRAY_FILTER_USE_BOTH);
|
|
?>
|
|
<style>
|
|
.dsl-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px}
|
|
.dsl-card{background:#f6f7f7;border:1px solid #c3c4c7;padding:6px;border-radius:4px;text-align:center}
|
|
.dsl-card.primary{background:#2271b1;border-color:#2271b1;color:#fff}
|
|
.dsl-num{font-size:18px;font-weight:600;line-height:1.2}
|
|
.dsl-lbl{font-size:12px;margin-top:4px;color:#50575e}
|
|
.dsl-card.primary .dsl-lbl{color:rgba(255,255,255,.85)}
|
|
.dsl-tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px}
|
|
.dsl-tag{background:#f6f7f7;border:1px solid #c3c4c7;padding:4px 10px;border-radius:3px;font-size:12px;color:#50575e}
|
|
.dsl-list{max-height:200px;overflow-y:auto}
|
|
.dsl-list table{width:100%;border-collapse:collapse}
|
|
.dsl-list td{padding:8px;border-bottom:1px solid #f0f0f1;font-size:13px}
|
|
.dsl-list tr:hover{background:#f6f7f7}
|
|
.dsl-list a{text-decoration:none;color:#2271b1}
|
|
.dsl-list a:hover{text-decoration:underline}
|
|
.dsl-list .type{color:#787c82;font-size:12px;margin-left:8px}
|
|
.dsl-list .count{font-weight:600;color:#1d2327}
|
|
.dsl-info{border-top:1px solid #c3c4c7;padding-top:12px;margin-top:16px;font-size:12px;color:#50575e}
|
|
.dsl-info code{background:#f0f0f1;padding:2px 6px;border-radius:3px;font-size:11px}
|
|
.dsl-header{display:flex;justify-content:space-between;align-items:center;margin:0 0 8px}
|
|
.dsl-reset{background:none;border:none;color:#787c82;font-size:12px;cursor:pointer;padding:0}
|
|
.dsl-reset:hover{color:#d63638;text-decoration:underline}
|
|
</style>
|
|
|
|
<div class="dsl-grid">
|
|
<div class="dsl-card primary">
|
|
<div class="dsl-num"><?php echo number_format_i18n($stats['today']); ?></div>
|
|
<div class="dsl-lbl">今日访问</div>
|
|
</div>
|
|
<div class="dsl-card">
|
|
<div class="dsl-num"><?php echo number_format_i18n($stats['yesterday']); ?></div>
|
|
<div class="dsl-lbl">昨日访问</div>
|
|
</div>
|
|
<div class="dsl-card">
|
|
<div class="dsl-num"><?php echo number_format_i18n($stats['week']); ?></div>
|
|
<div class="dsl-lbl">近 7 天</div>
|
|
</div>
|
|
<div class="dsl-card">
|
|
<div class="dsl-num"><?php echo number_format_i18n($stats['total']); ?></div>
|
|
<div class="dsl-lbl">累计访问</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dsl-header">
|
|
<h4 style="margin:0;font-size:13px;color:#1d2327">按类型统计</h4>
|
|
<form method="post" style="margin:0" onsubmit="return confirm('确定要重置所有统计数据吗?此操作不可撤销。');">
|
|
<?php wp_nonce_field('discourse_shortlink_reset_nonce'); ?>
|
|
<button type="submit" name="discourse_shortlink_reset" value="1" class="dsl-reset">重置</button>
|
|
</form>
|
|
</div>
|
|
<?php if (!empty($stats['by_type'])): ?>
|
|
<div class="dsl-tags">
|
|
<?php foreach ($stats['by_type'] as $t => $c): ?>
|
|
<span class="dsl-tag"><?php echo esc_html($labels[$t] ?? $t); ?> <strong><?php echo number_format_i18n($c); ?></strong></span>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php else: ?>
|
|
<p style="color:#787c82;font-size:12px;margin:8px 0">暂无统计数据</p>
|
|
<?php endif; ?>
|
|
|
|
<?php if (!empty($stats['top_links'])): ?>
|
|
<h4 style="margin:0 0 8px;font-size:13px;color:#1d2327">热门链接</h4>
|
|
<div class="dsl-list"><table>
|
|
<?php foreach ($stats['top_links'] as $key => $cnt):
|
|
list($t, $tgt) = explode(':', $key, 2);
|
|
$short = match($t) {
|
|
'topic' => "/t/$tgt",
|
|
'category' => "/c/$tgt",
|
|
'user' => "/u/$tgt",
|
|
'post' => "/p/$tgt",
|
|
'group' => "/g/$tgt",
|
|
'tag' => "/h/$tgt",
|
|
default => "/$t/$tgt"
|
|
};
|
|
$full = match($t) {
|
|
'topic' => DISCOURSE_SHORTLINK_URL . "/t/-/$tgt",
|
|
'category' => DISCOURSE_SHORTLINK_URL . "/c/$tgt",
|
|
'user' => DISCOURSE_SHORTLINK_URL . "/u/$tgt",
|
|
'post' => DISCOURSE_SHORTLINK_URL . "/p/$tgt",
|
|
'group' => DISCOURSE_SHORTLINK_URL . "/g/$tgt",
|
|
'tag' => DISCOURSE_SHORTLINK_URL . "/tag/$tgt",
|
|
default => DISCOURSE_SHORTLINK_URL . "/$t/$tgt"
|
|
};
|
|
?>
|
|
<tr>
|
|
<td>
|
|
<a href="<?php echo esc_url($full); ?>" target="_blank"><?php echo esc_html($short); ?></a>
|
|
<span class="type"><?php echo esc_html($labels[$t] ?? $t); ?></span>
|
|
</td>
|
|
<td style="text-align:right">
|
|
<span class="count"><?php echo number_format_i18n($cnt); ?></span>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</table></div>
|
|
<?php endif; ?>
|
|
|
|
<div class="dsl-info">
|
|
<code>/t/123</code> 话题
|
|
<code>/c/slug</code> 分类
|
|
<code>/u/user</code> 用户
|
|
<code>/p/123</code> 帖子
|
|
<code>/g/name</code> 群组
|
|
<code>/h/tag</code> 标签
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* REST API
|
|
*/
|
|
add_action('rest_api_init', function() {
|
|
register_rest_route('discourse/v1', '/shortlink-stats', [
|
|
'methods' => 'GET',
|
|
'callback' => fn() => discourse_shortlink_get_stats(),
|
|
'permission_callback' => fn() => current_user_can('manage_options')
|
|
]);
|
|
|
|
register_rest_route('discourse/v1', '/shortlink-reset', [
|
|
'methods' => 'POST',
|
|
'callback' => fn() => ['success' => discourse_shortlink_reset_stats()],
|
|
'permission_callback' => fn() => current_user_can('manage_options')
|
|
]);
|
|
});
|