discourse-shortlinks/discourse-shortlinks.php
feibisi 527ab8e4e8 feat: Discourse 短链接重定向插件 v1.1.0
WordPress mu-plugin,支持 /t/ /c/ /u/ /p/ /g/ /h/ 六种短链格式
301 重定向到 Discourse 社区,带访问统计和仪表盘小工具

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-23 16:51:53 +08:00

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> 话题 &nbsp;
<code>/c/slug</code> 分类 &nbsp;
<code>/u/user</code> 用户 &nbsp;
<code>/p/123</code> 帖子 &nbsp;
<code>/g/name</code> 群组 &nbsp;
<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')
]);
});