在内容管理系统中,防止发表重复标题的文章是确保内容质量和SEO表现的重要环节。本文提供2026年WordPress中防止重复标题文章的完整代码解决方案。
一、问题分析与设计思路
为什么需要防止重复标题?
- SEO优化:重复标题导致内容重复,影响搜索引擎排名
- 用户体验:用户混淆相似内容,降低阅读体验
- 数据管理:避免数据冗余,便于内容管理
- 品牌形象:专业网站应避免重复内容
技术实现思路
用户提交 → 标题检查 → 查重验证 → 阻止/警告 → 处理完成
↓
数据库查询
↓
相似度计算
↓
智能建议
二、基础防止重复方案
方案1:发布前检查(最常用)
核心检查函数
/**
* 检查文章标题是否重复
* @param string $title 文章标题
* @param int $post_id 当前文章ID(编辑时使用)
* @param string $post_type 文章类型
* @return array 检查结果
*/
function check_duplicate_post_title($title, $post_id = 0, $post_type = 'post') {
global $wpdb;
// 清理标题
$clean_title = sanitize_text_field($title);
$clean_title = trim($clean_title);
if (empty($clean_title)) {
return [
'is_duplicate' => false,
'message' => '标题不能为空',
'similar_posts' => []
];
}
// 准备SQL查询
$query = $wpdb->prepare("
SELECT ID, post_title, post_status, post_date
FROM {$wpdb->posts}
WHERE post_title = %s
AND post_type = %s
AND post_status IN ('publish', 'pending', 'draft', 'future', 'private')
", $clean_title, $post_type);
// 如果是编辑文章,排除自己
if ($post_id > 0) {
$query .= $wpdb->prepare(" AND ID != %d", $post_id);
}
$existing_posts = $wpdb->get_results($query);
if (!empty($existing_posts)) {
$formatted_posts = [];
foreach ($existing_posts as $post) {
$formatted_posts[] = [
'id' => $post->ID,
'title' => $post->post_title,
'status' => $post->post_status,
'date' => $post->post_date,
'edit_link' => get_edit_post_link($post->ID),
'view_link' => get_permalink($post->ID)
];
}
return [
'is_duplicate' => true,
'message' => '发现重复标题的文章',
'similar_posts' => $formatted_posts,
'count' => count($existing_posts)
];
}
return [
'is_duplicate' => false,
'message' => '标题可用',
'similar_posts' => []
];
}
添加到文章保存钩子
/**
* 保存文章时检查重复标题
*/
function prevent_duplicate_title_on_save($data, $postarr) {
// 只检查特定文章类型
$check_post_types = apply_filters('duplicate_title_check_post_types', ['post', 'page']);
if (!in_array($data['post_type'], $check_post_types)) {
return $data;
}
// 获取标题
$title = isset($data['post_title']) ? $data['post_title'] : '';
if (empty($title)) {
return $data;
}
// 检查重复
$post_id = isset($postarr['ID']) ? intval($postarr['ID']) : 0;
$result = check_duplicate_post_title($title, $post_id, $data['post_type']);
if ($result['is_duplicate']) {
// 阻止保存并显示错误
$error_message = '错误:存在重复标题的文章。<br>';
foreach ($result['similar_posts'] as $similar) {
$status_text = get_post_status_object($similar['status'])->label;
$error_message .= sprintf(
'• <a href="%s" target="_blank">%s</a> (%s, 发布于%s)<br>',
esc_url($similar['edit_link']),
esc_html($similar['title']),
$status_text,
date('Y-m-d', strtotime($similar['date']))
);
}
// 保存错误到会话或transient
set_transient('duplicate_title_error_' . get_current_user_id(), $error_message, 30);
// 添加管理错误
if (is_admin()) {
add_action('admin_notices', function() use ($error_message) {
echo '<div class="notice notice-error is-dismissible"><p>' . $error_message . '</p></div>';
});
}
// 返回错误阻止保存
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return $data;
}
// 在后台直接阻止
if (is_admin()) {
wp_die($error_message, '重复标题错误', ['back_link' => true]);
}
}
return $data;
}
// 高优先级,在保存前检查
add_filter('wp_insert_post_data', 'prevent_duplicate_title_on_save', 99, 2);
方案2:AJAX实时检查
/**
* 添加AJAX实时标题检查
*/
function enqueue_title_check_scripts() {
global $pagenow, $post_type;
$check_pages = ['post.php', 'post-new.php'];
if (in_array($pagenow, $check_pages)) {
wp_enqueue_script('title-check-js',
get_template_directory_uri() . '/js/title-check.js',
['jquery'],
'1.0.0',
true
);
wp_localize_script('title-check-js', 'titleCheckAjax', [
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('title_check_nonce'),
'checking_text' => '检查标题中...',
'available_text' => '标题可用',
'duplicate_text' => '标题已存在',
'similar_text' => '发现相似标题'
]);
}
}
add_action('admin_enqueue_scripts', 'enqueue_title_check_scripts');
// AJAX处理
add_action('wp_ajax_check_post_title', 'ajax_check_post_title');
function ajax_check_post_title() {
// 验证nonce
check_ajax_referer('title_check_nonce', 'nonce');
$title = isset($_POST['title']) ? sanitize_text_field($_POST['title']) : '';
$post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
$post_type = isset($_POST['post_type']) ? sanitize_text_field($_POST['post_type']) : 'post';
if (empty($title)) {
wp_send_json_error(['message' => '标题不能为空']);
}
$result = check_duplicate_post_title($title, $post_id, $post_type);
if ($result['is_duplicate']) {
wp_send_json_success([
'is_duplicate' => true,
'message' => '发现重复标题',
'similar_posts' => $result['similar_posts']
]);
} else {
wp_send_json_success([
'is_duplicate' => false,
'message' => '标题可用'
]);
}
}
前端JavaScript
// title-check.js
(function($) {
'use strict';
var titleCheck = {
init: function() {
this.cacheElements();
this.bindEvents();
},
cacheElements: function() {
this.$titleInput = $('#title');
this.$titleWrapper = $('#titlediv');
this.checkTimer = null;
this.currentTitle = '';
},
bindEvents: function() {
if (!this.$titleInput.length) return;
// 输入时检查
this.$titleInput.on('input', $.proxy(this.onTitleChange, this));
// 发布时最后检查
$('#publish').on('click', $.proxy(this.onPublishClick, this));
},
onTitleChange: function(e) {
var newTitle = $(e.target).val().trim();
// 避免频繁检查
if (newTitle === this.currentTitle || newTitle.length < 3) {
this.removeStatus();
return;
}
this.currentTitle = newTitle;
clearTimeout(this.checkTimer);
this.checkTimer = setTimeout($.proxy(this.checkTitle, this), 500);
},
checkTitle: function() {
var title = this.currentTitle;
if (title.length < 3) {
this.removeStatus();
return;
}
this.showChecking();
$.ajax({
url: titleCheckAjax.ajax_url,
type: 'POST',
data: {
action: 'check_post_title',
nonce: titleCheckAjax.nonce,
title: title,
post_id: $('#post_ID').val() || 0,
post_type: $('#post_type').val() || 'post'
},
success: $.proxy(this.onCheckSuccess, this),
error: $.proxy(this.onCheckError, this)
});
},
onCheckSuccess: function(response) {
if (!response.success) {
this.showError('检查失败');
return;
}
var data = response.data;
if (data.is_duplicate) {
this.showDuplicate(data);
} else {
this.showAvailable();
}
},
onCheckError: function() {
this.showError('网络错误,请重试');
},
showChecking: function() {
this.removeStatus();
this.$titleWrapper.append(
'<div class="title-check-status checking">' +
'<span class="dashicons dashicons-update"></span> ' +
titleCheckAjax.checking_text +
'</div>'
);
},
showAvailable: function() {
this.removeStatus();
this.$titleWrapper.append(
'<div class="title-check-status available">' +
'<span class="dashicons dashicons-yes"></span> ' +
titleCheckAjax.available_text +
'</div>'
);
},
showDuplicate: function(data) {
this.removeStatus();
var $status = $(
'<div class="title-check-status duplicate">' +
'<span class="dashicons dashicons-no"></span> ' +
titleCheckAjax.duplicate_text +
'</div>'
);
if (data.similar_posts && data.similar_posts.length > 0) {
var $list = $('<ul class="duplicate-posts-list"></ul>');
$.each(data.similar_posts, function(i, post) {
var statusText = post.status === 'publish' ? '已发布' :
post.status === 'draft' ? '草稿' :
post.status === 'pending' ? '待审核' : post.status;
$list.append(
'<li>' +
'<a href="' + post.edit_link + '" target="_blank" class="edit-link">编辑</a> | ' +
'<a href="' + post.view_link + '" target="_blank" class="view-link">查看</a> | ' +
'<span class="post-status">' + statusText + '</span> | ' +
'<span class="post-date">' + post.date + '</span>' +
'</li>'
);
});
$status.append($list);
}
this.$titleWrapper.append($status);
},
showError: function(message) {
this.removeStatus();
this.$titleWrapper.append(
'<div class="title-check-status error">' +
'<span class="dashicons dashicons-warning"></span> ' +
message +
'</div>'
);
},
removeStatus: function() {
$('.title-check-status').remove();
},
onPublishClick: function(e) {
var title = this.$titleInput.val().trim();
if (title.length < 3) {
e.preventDefault();
alert('请输入有效的标题');
return;
}
// 同步检查
$.ajax({
url: titleCheckAjax.ajax_url,
type: 'POST',
async: false,
data: {
action: 'check_post_title',
nonce: titleCheckAjax.nonce,
title: title,
post_id: $('#post_ID').val() || 0,
post_type: $('#post_type').val() || 'post'
},
success: function(response) {
if (response.success && response.data.is_duplicate) {
e.preventDefault();
var message = '存在重复标题,请修改后重试。\n\n重复文章:\n';
$.each(response.data.similar_posts, function(i, post) {
message += (i+1) + '. ' + post.title + '\n';
});
alert(message);
}
}
});
}
};
$(document).ready(function() {
titleCheck.init();
});
})(jQuery);
CSS样式
.title-check-status {
padding: 8px 12px;
margin: 10px 0;
border-radius: 4px;
font-size: 13px;
}
.title-check-status.checking {
background-color: #f0f0f1;
border-left: 4px solid #72aee6;
}
.title-check-status.available {
background-color: #edfaef;
border-left: 4px solid #00a32a;
}
.title-check-status.duplicate {
background-color: #fcf0f1;
border-left: 4px solid #d63638;
}
.title-check-status.error {
background-color: #fcf9e8;
border-left: 4px solid #dba617;
}
.title-check-status .dashicons {
margin-right: 5px;
vertical-align: middle;
}
.duplicate-posts-list {
margin: 8px 0 0 0;
padding-left: 20px;
}
.duplicate-posts-list li {
margin: 5px 0;
padding: 3px 0;
border-bottom: 1px dashed #ddd;
}
.duplicate-posts-list li:last-child {
border-bottom: none;
}
.duplicate-posts-list a {
text-decoration: none;
}
.duplicate-posts-list .edit-link {
color: #2271b1;
}
.duplicate-posts-list .view-link {
color: #50575e;
}
.duplicate-posts-list .post-status,
.duplicate-posts-list .post-date {
color: #8c8f94;
font-size: 12px;
margin-left: 8px;
}
三、高级查重方案
方案3:智能相似度检查
/**
* 智能标题相似度检查
* 使用多种算法计算相似度
*/
class TitleSimilarityChecker {
/**
* 计算标题相似度
* @param string $title1 标题1
* @param string $title2 标题2
* @return float 相似度分数 0-1
*/
public static function calculate_similarity($title1, $title2) {
$title1 = self::normalize_title($title1);
$title2 = self::normalize_title($title2);
if ($title1 === $title2) {
return 1.0;
}
// 使用多种算法计算相似度
$algorithms = [
'levenshtein' => self::similarity_levenshtein($title1, $title2),
'jaro_winkler' => self::similarity_jaro_winkler($title1, $title2),
'cosine' => self::similarity_cosine($title1, $title2)
];
// 加权平均
$weights = [
'levenshtein' => 0.3,
'jaro_winkler' => 0.4,
'cosine' => 0.3
];
$total_score = 0;
foreach ($algorithms as $algorithm => $score) {
$total_score += $score * $weights[$algorithm];
}
return $total_score;
}
/**
* 标准化标题
*/
private static function normalize_title($title) {
$title = strtolower(trim($title));
// 移除标点符号
$title = preg_replace('/[^\p{L}\p{N}\s]/u', '', $title);
// 移除多余空格
$title = preg_replace('/\s+/', ' ', $title);
// 移除停用词
$stop_words = ['的', '了', '在', '是', '我', '有', '和', '就',
'不', '人', '都', '一', '一个', '上', '也', '很',
'到', '说', '要', '去', '你', '会', '着', '没有',
'看', '好', '自己', '这'];
$words = explode(' ', $title);
$words = array_diff($words, $stop_words);
return implode(' ', $words);
}
/**
* Levenshtein距离算法
*/
private static function similarity_levenshtein($str1, $str2) {
$len1 = mb_strlen($str1, 'UTF-8');
$len2 = mb_strlen($str2, 'UTF-8');
if ($len1 == 0 || $len2 == 0) {
return 0;
}
$max_len = max($len1, $len2);
$distance = levenshtein($str1, $str2);
return 1 - ($distance / $max_len);
}
/**
* Jaro-Winkler算法
*/
private static function similarity_jaro_winkler($str1, $str2) {
$len1 = mb_strlen($str1, 'UTF-8');
$len2 = mb_strlen($str2, 'UTF-8');
if ($len1 == 0 || $len2 == 0) {
return 0;
}
$match_distance = (int) floor(max($len1, $len2) / 2) - 1;
$str1_matches = array_fill(0, $len1, false);
$str2_matches = array_fill(0, $len2, false);
$matches = 0;
$transpositions = 0;
for ($i = 0; $i < $len1; $i++) {
$start = max(0, $i - $match_distance);
$end = min($i + $match_distance + 1, $len2);
for ($j = $start; $j < $end; $j++) {
if ($str2_matches[$j]) continue;
if (mb_substr($str1, $i, 1, 'UTF-8') == mb_substr($str2, $j, 1, 'UTF-8')) {
$str1_matches[$i] = true;
$str2_matches[$j] = true;
$matches++;
break;
}
}
}
if ($matches == 0) return 0;
$k = 0;
for ($i = 0; $i < $len1; $i++) {
if (!$str1_matches[$i]) continue;
while (!$str2_matches[$k]) $k++;
if (mb_substr($str1, $i, 1, 'UTF-8') != mb_substr($str2, $k, 1, 'UTF-8')) {
$transpositions++;
}
$k++;
}
$transpositions /= 2;
$jaro = (($matches / $len1) + ($matches / $len2) + (($matches - $transpositions) / $matches)) / 3;
// Winkler调整
$prefix = 0;
$max_prefix = min(4, min($len1, $len2));
for ($i = 0; $i < $max_prefix; $i++) {
if (mb_substr($str1, $i, 1, 'UTF-8') == mb_substr($str2, $i, 1, 'UTF-8')) {
$prefix++;
} else {
break;
}
}
$jaro_winkler = $jaro + ($prefix * 0.1 * (1 - $jaro));
return $jaro_winkler;
}
/**
* 余弦相似度算法
*/
private static function similarity_cosine($str1, $str2) {
$words1 = explode(' ', $str1);
$words2 = explode(' ', $str2);
$all_words = array_unique(array_merge($words1, $words2));
$vector1 = array_fill_keys($all_words, 0);
$vector2 = array_fill_keys($all_words, 0);
foreach ($words1 as $word) {
$vector1[$word]++;
}
foreach ($words2 as $word) {
$vector2[$word]++;
}
$dot_product = 0;
$magnitude1 = 0;
$magnitude2 = 0;
foreach ($all_words as $word) {
$dot_product += $vector1[$word] * $vector2[$word];
$magnitude1 += pow($vector1[$word], 2);
$magnitude2 += pow($vector2[$word], 2);
}
$magnitude1 = sqrt($magnitude1);
$magnitude2 = sqrt($magnitude2);
if ($magnitude1 == 0 || $magnitude2 == 0) {
return 0;
}
return $dot_product / ($magnitude1 * $magnitude2);
}
/**
* 查找相似标题的文章
*/
public static function find_similar_titles($title, $threshold = 0.8, $limit = 5, $exclude_id = 0) {
global $wpdb;
$normalized_title = self::normalize_title($title);
// 获取所有文章标题进行比较
$query = $wpdb->prepare("
SELECT ID, post_title, post_status, post_date
FROM {$wpdb->posts}
WHERE post_type = 'post'
AND post_status IN ('publish', 'pending', 'draft', 'future', 'private')
");
if ($exclude_id > 0) {
$query .= $wpdb->prepare(" AND ID != %d", $exclude_id);
}
$all_posts = $wpdb->get_results($query);
$similar_posts = [];
foreach ($all_posts as $post) {
$similarity = self::calculate_similarity($title, $post->post_title);
if ($similarity >= $threshold) {
$similar_posts[] = [
'post' => $post,
'similarity' => round($similarity * 100, 1)
];
}
}
// 按相似度排序
usort($similar_posts, function($a, $b) {
return $b['similarity'] <=> $a['similarity'];
});
// 限制数量
$similar_posts = array_slice($similar_posts, 0, $limit);
return $similar_posts;
}
}
方案4:基于相似度的保存检查
/**
* 使用智能相似度检查防止重复
*/
function prevent_similar_titles_on_save($data, $postarr) {
$check_post_types = apply_filters('similar_title_check_post_types', ['post', 'page']);
if (!in_array($data['post_type'], $check_post_types)) {
return $data;
}
$title = isset($data['post_title']) ? $data['post_title'] : '';
if (empty($title) || strlen($title) < 5) {
return $data;
}
$post_id = isset($postarr['ID']) ? intval($postarr['ID']) : 0;
$threshold = get_option('title_similarity_threshold', 0.85);
$similar_posts = TitleSimilarityChecker::find_similar_titles($title, $threshold, 5, $post_id);
if (!empty($similar_posts)) {
$highest_similarity = $similar_posts[0]['similarity'];
// 根据相似度阈值决定是否阻止
if ($highest_similarity >= ($threshold * 100)) {
$error_message = sprintf(
'警告:发现高度相似的文章标题(相似度%.1f%%)。<br>',
$highest_similarity
);
$error_message .= '相似文章:<br>';
foreach ($similar_posts as $item) {
$post = $item['post'];
$similarity = $item['similarity'];
$status_text = get_post_status_object($post->post_status)->label;
$edit_link = get_edit_post_link($post->ID);
$error_message .= sprintf(
'• <a href="%s" target="_blank">%s</a> (相似度%.1f%%, %s)<br>',
esc_url($edit_link),
esc_html($post->post_title),
$similarity,
$status_text
);
}
$error_message .= '<br>是否继续保存?<br>';
$error_message .= '<button type="button" class="button force-save">强制保存</button>';
set_transient('similar_title_error_' . get_current_user_id(), $error_message, 30);
// 在后台显示警告但不阻止
if (is_admin()) {
add_action('admin_notices', function() use ($error_message) {
?>
<div class="notice notice-warning is-dismissible similar-title-warning">
<p><?php echo $error_message; ?></p>
</div>
<script>
jQuery(document).ready(function($) {
$('.similar-title-warning .force-save').on('click', function() {
$(this).closest('.notice').fadeOut();
// 添加隐藏字段标记强制保存
$('#title').after('<input type="hidden" name="force_save_similar_title" value="1">');
});
});
</script>
<?php
});
// 检查是否强制保存
if (!isset($_POST['force_save_similar_title']) || $_POST['force_save_similar_title'] != '1') {
// 不阻止保存,但记录日志
error_log(sprintf(
'相似标题警告:文章"%s" (ID: %s) 与已有文章相似',
$title,
$post_id
));
}
}
}
}
return $data;
}
add_filter('wp_insert_post_data', 'prevent_similar_titles_on_save', 100, 2);
四、批量检查和清理
方案5:批量查重工具
/**
* 批量查重工具类
*/
class BulkTitleDuplicateChecker {
/**
* 扫描所有文章的重复标题
*/
public static function scan_all_duplicates($post_type = 'post') {
global $wpdb;
$query = $wpdb->prepare("
SELECT post_title, COUNT(*) as count,
GROUP_CONCAT(ID ORDER BY post_date DESC) as post_ids
FROM {$wpdb->posts}
WHERE post_type = %s
AND post_status IN ('publish', 'pending', 'draft', 'future', 'private')
GROUP BY post_title
HAVING count > 1
ORDER BY count DESC
", $post_type);
$results = $wpdb->get_results($query);
$duplicates = [];
foreach ($results as $row) {
$post_ids = explode(',', $row->post_ids);
$post_details = [];
foreach ($post_ids as $post_id) {
$post = get_post($post_id);
if ($post) {
$post_details[] = [
'id' => $post->ID,
'title' => $post->post_title,
'status' => $post->post_status,
'date' => $post->post_date,
'edit_link' => get_edit_post_link($post->ID),
'view_link' => get_permalink($post->ID)
];
}
}
$duplicates[] = [
'title' => $row->post_title,
'count' => $row->count,
'posts' => $post_details
];
}
return $duplicates;
}
/**
* 生成查重报告
*/
public static function generate_report($duplicates) {
$report = "文章标题查重报告\n";
$report .= "生成时间: " . current_time('Y-m-d H:i:s') . "\n";
$report .= "总共发现重复组: " . count($duplicates) . "\n";
$report .= "====================\n\n";
foreach ($duplicates as $index => $group) {
$report .= sprintf("第%d组: %s (重复%d次)\n",
$index + 1,
$group['title'],
$group['count']
);
foreach ($group['posts'] as $post) {
$status_text = get_post_status_object($post['status'])->label;
$report .= sprintf(" - ID: %d, 状态: %s, 发布时间: %s\n",
$post['id'],
$status_text,
$post['date']
);
}
$report .= "\n";
}
return $report;
}
/**
* 批量合并建议
*/
public static function suggest_merge($duplicate_group) {
$posts = $duplicate_group['posts'];
// 按状态和日期排序,保留最新发布的文章
usort($posts, function($a, $b) {
// 已发布的优先
if ($a['status'] === 'publish' && $b['status'] !== 'publish') return -1;
if ($b['status'] === 'publish' && $a['status'] !== 'publish') return 1;
// 按日期倒序
return strtotime($b['date']) <=> strtotime($a['date']);
});
$keep_post = $posts[0];
$merge_posts = array_slice($posts, 1);
return [
'keep' => $keep_post,
'merge' => $merge_posts,
'suggest_action' => '保留最新发布的文章(ID: ' . $keep_post['id'] . '),合并或删除其他文章'
];
}
}
/**
* 管理后台批量查重页面
*/
function add_duplicate_checker_page() {
add_management_page(
'文章标题查重',
'标题查重',
'manage_options',
'title-duplicate-checker',
'display_duplicate_checker_page'
);
}
add_action('admin_menu', 'add_duplicate_checker_page');
function display_duplicate_checker_page() {
?>
<div class="wrap">
<h1>文章标题查重工具</h1>
<?php
if (isset($_GET['action']) && $_GET['action'] === 'scan') {
$post_type = isset($_GET['post_type']) ? sanitize_text_field($_GET['post_type']) : 'post';
$duplicates = BulkTitleDuplicateChecker::scan_all_duplicates($post_type);
if (empty($duplicates)) {
echo '<div class="notice notice-success"><p>未发现重复标题的文章。</p></div>';
} else {
echo '<div class="notice notice-warning"><p>发现' . count($duplicates) . '组重复标题。</p></div>';
echo '<table class="wp-list-table widefat fixed striped">';
echo '<thead>';
echo '<tr>';
echo '<th width="30%">标题</th>';
echo '<th width="10%">重复数</th>';
echo '<th width="40%">文章列表</th>';
echo '<th width="20%">操作</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ($duplicates as $group) {
echo '<tr>';
echo '<td><strong>' . esc_html($group['title']) . '</strong></td>';
echo '<td>' . $group['count'] . '</td>';
echo '<td>';
foreach ($group['posts'] as $post) {
$status_text = get_post_status_object($post['status'])->label;
echo '<div>';
echo '<a href="' . esc_url($post['edit_link']) . '">编辑</a> | ';
echo '<a href="' . esc_url($post['view_link']) . '" target="_blank">查看</a> | ';
echo 'ID: ' . $post['id'] . ' | ';
echo '状态: ' . $status_text . ' | ';
echo '时间: ' . $post['date'];
echo '</div>';
}
echo '</td>';
echo '<td>';
$suggestion = BulkTitleDuplicateChecker::suggest_merge($group);
echo '<button class="button button-small show-suggestion"
data-keep-id="' . $suggestion['keep']['id'] . '"
data-merge-ids="' . implode(',', array_column($suggestion['merge'], 'id')) . '">查看建议</button>';
echo '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
// 下载报告按钮
echo '<form method="post" style="margin-top: 20px;">';
echo '<input type="hidden" name="download_report" value="1">';
echo '<input type="hidden" name="report_data" value=\'' . json_encode($duplicates) . '\'>';
wp_nonce_field('download_report_nonce', 'report_nonce');
echo '<button type="submit" class="button button-primary">下载查重报告</button>';
echo '</form>';
}
}
?>
<div class="card" style="max-width: 600px; margin-top: 20px;">
<h2>扫描重复标题</h2>
<form method="get">
<input type="hidden" name="page" value="title-duplicate-checker">
<input type="hidden" name="action" value="scan">
<table class="form-table">
<tr>
<th scope="row">文章类型</th>
<td>
<select name="post_type">
<option value="post">文章</option>
<option value="page">页面</option>
<?php
$custom_post_types = get_post_types(['public' => true, '_builtin' => false]);
foreach ($custom_post_types as $post_type) {
echo '<option value="' . esc_attr($post_type) . '">' . esc_html($post_type) . '</option>';
}
?>
</select>
</td>
</tr>
</table>
<?php submit_button('开始扫描'); ?>
</form>
</div>
</div>
<script>
jQuery(document).ready(function($) {
$('.show-suggestion').on('click', function() {
var keepId = $(this).data('keep-id');
var mergeIds = $(this).data('merge-ids').split(',');
var message = '建议操作:\n';
message += '保留文章(ID: ' + keepId + ')\n';
message += '处理以下重复文章:\n';
mergeIds.forEach(function(id) {
message += ' - ID: ' + id + '\n';
});
message += '\n是否执行合并操作?';
if (confirm(message)) {
// 这里可以添加AJAX合并操作
alert('合并功能需要进一步开发');
}
});
});
</script>
<?php
}
// 处理报告下载
add_action('admin_init', function() {
if (isset($_POST['download_report']) && $_POST['download_report'] == '1') {
check_admin_referer('download_report_nonce', 'report_nonce');
$report_data = json_decode(stripslashes($_POST['report_data']), true);
$report = BulkTitleDuplicateChecker::generate_report($report_data);
header('Content-Type: text/plain');
header('Content-Disposition: attachment; filename="title-duplicates-report-' . date('Ymd-His') . '.txt"');
echo $report;
exit;
}
});
五、数据库层面防止重复
方案6:数据库唯一约束
/**
* 添加数据库唯一约束
* 注意:这会影响现有数据,需要谨慎操作
*/
function add_unique_title_constraint() {
global $wpdb;
$table_name = $wpdb->posts;
// 检查是否已存在约束
$result = $wpdb->get_var("
SELECT COUNT(*)
FROM information_schema.table_constraints
WHERE table_name = '{$wpdb->posts}'
AND constraint_name = 'unique_post_title'
AND constraint_type = 'UNIQUE'
");
if ($result == 0) {
// 添加唯一约束
$sql = "ALTER TABLE {$table_name}
ADD CONSTRAINT unique_post_title
UNIQUE (post_title, post_type, post_status)";
$wpdb->query($sql);
return true;
}
return false;
}
// 更安全的方案:添加唯一索引而不是约束
function add_unique_title_index() {
global $wpdb;
$index_name = 'idx_unique_post_title';
// 检查是否已存在索引
$result = $wpdb->get_var("
SHOW INDEX FROM {$wpdb->posts}
WHERE Key_name = '{$index_name}'
");
if (!$result) {
// 创建唯一索引
$sql = "CREATE UNIQUE INDEX {$index_name}
ON {$wpdb->posts} (post_title(191), post_type, post_status)";
$wpdb->query($sql);
}
}
// 在插件激活时执行
register_activation_hook(__FILE__, 'add_unique_title_index');
六、完整插件实现
Title Duplicate Preventer 2026 完整插件
<?php
/**
* Plugin Name: Title Duplicate Preventer 2026
* Plugin URI: https://yourwebsite.com/
* Description: 防止发表重复或相似标题的文章
* Version: 2.0.0
* Author: Your Name
* License: GPL v2 or later
* Text Domain: title-duplicate-preventer
*/
// 防止直接访问
if (!defined('ABSPATH')) {
exit;
}
class Title_Duplicate_Preventer_2026 {
private static $instance = null;
private $similarity_threshold = 0.85;
private $check_exact_match = true;
private $check_similar_match = true;
private $force_check = false;
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->init_hooks();
$this->load_settings();
}
private function init_hooks() {
// 核心检查钩子
add_filter('wp_insert_post_data', [$this, 'check_duplicate_on_save'], 99, 2);
// AJAX检查
add_action('wp_ajax_tdp_check_title', [$this, 'ajax_check_title']);
add_action('wp_ajax_nopriv_tdp_check_title', [$this, 'ajax_check_title_nopriv']);
// 管理界面
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
add_action('admin_menu', [$this, 'add_admin_menu']);
add_action('admin_init', [$this, 'register_settings']);
// 批量工具
add_action('admin_post_tdp_bulk_check', [$this, 'handle_bulk_check']);
// 短代码
add_shortcode('tdp_title_check', [$this, 'shortcode_title_check']);
// REST API
add_action('rest_api_init', [$this, 'register_rest_routes']);
}
private function load_settings() {
$this->similarity_threshold = get_option('tdp_similarity_threshold', 0.85);
$this->check_exact_match = get_option('tdp_check_exact', true);
$this->check_similar_match = get_option('tdp_check_similar', true);
$this->force_check = get_option('tdp_force_check', false);
}
public function check_duplicate_on_save($data, $postarr) {
// 跳过自动保存、修订等
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return $data;
}
if (wp_is_post_revision($postarr['ID'])) {
return $data;
}
// 检查是否应该跳过
if (isset($_POST['tdp_skip_check']) && $_POST['tdp_skip_check'] == '1') {
return $data;
}
$post_type = $data['post_type'];
$title = isset($data['post_title']) ? trim($data['post_title']) : '';
$post_id = isset($postarr['ID']) ? intval($postarr['ID']) : 0;
// 检查是否在允许的类型中
$allowed_types = apply_filters('tdp_allowed_post_types', ['post', 'page']);
if (!in_array($post_type, $allowed_types)) {
return $data;
}
if (empty($title)) {
return $data;
}
$errors = [];
// 检查完全重复
if ($this->check_exact_match) {
$exact_result = $this->check_exact_duplicate($title, $post_id, $post_type);
if ($exact_result['found']) {
$errors[] = [
'type' => 'exact',
'message' => '发现完全相同的标题',
'posts' => $exact_result['posts']
];
}
}
// 检查相似标题
if ($this->check_similar_match && empty($errors)) {
$similar_result = $this->check_similar_titles($title, $post_id, $post_type);
if (!empty($similar_result['posts'])) {
$errors[] = [
'type' => 'similar',
'message' => sprintf('发现相似标题(相似度%.1f%%)', $similar_result['highest_similarity']),
'posts' => $similar_result['posts'],
'similarity' => $similar_result['highest_similarity']
];
}
}
// 处理错误
if (!empty($errors)) {
$this->handle_duplicate_errors($errors, $title, $post_id);
// 如果强制检查,阻止保存
if ($this->force_check) {
$error_message = '无法保存:存在重复或相似标题。';
if (defined('DOING_AJAX') && DOING_AJAX) {
wp_send_json_error(['message' => $error_message]);
} else {
wp_die($error_message, '标题重复错误', ['back_link' => true]);
}
}
}
return $data;
}
private function check_exact_duplicate($title, $exclude_id = 0, $post_type = 'post') {
global $wpdb;
$query = $wpdb->prepare("
SELECT ID, post_title, post_status, post_date
FROM {$wpdb->posts}
WHERE post_title = %s
AND post_type = %s
AND post_status IN ('publish', 'pending', 'draft', 'future', 'private')
", $title, $post_type);
if ($exclude_id > 0) {
$query .= $wpdb->prepare(" AND ID != %d", $exclude_id);
}
$posts = $wpdb->get_results($query);
if (empty($posts)) {
return ['found' => false, 'posts' => []];
}
$formatted_posts = [];
foreach ($posts as $post) {
$formatted_posts[] = [
'id' => $post->ID,
'title' => $post->post_title,
'status' => $post->post_status,
'date' => $post->post_date,
'edit_link' => get_edit_post_link($post->ID)
];
}
return ['found' => true, 'posts' => $formatted_posts];
}
private function check_similar_titles($title, $exclude_id = 0, $post_type = 'post', $threshold = null) {
if ($threshold === null) {
$threshold = $this->similarity_threshold;
}
global $wpdb;
// 获取所有同类型文章
$query = $wpdb->prepare("
SELECT ID, post_title, post_status, post_date
FROM {$wpdb->posts}
WHERE post_type = %s
AND post_status IN ('publish', 'pending', 'draft', 'future', 'private')
", $post_type);
if ($exclude_id > 0) {
$query .= $wpdb->prepare(" AND ID != %d", $exclude_id);
}
$all_posts = $wpdb->get_results($query);
$similar_posts = [];
$highest_similarity = 0;
foreach ($all_posts as $post) {
$similarity = $this->calculate_similarity($title, $post->post_title);
if ($similarity >= $threshold) {
$similar_posts[] = [
'id' => $post->ID,
'title' => $post->post_title,
'status' => $post->post_status,
'date' => $post->post_date,
'edit_link' => get_edit_post_link($post->ID),
'similarity' => round($similarity * 100, 1)
];
if ($similarity > $highest_similarity) {
$highest_similarity = $similarity;
}
}
}
// 按相似度排序
usort($similar_posts, function($a, $b) {
return $b['similarity'] <=> $a['similarity'];
});
return [
'posts' => $similar_posts,
'highest_similarity' => round($highest_similarity * 100, 1)
];
}
private function calculate_similarity($str1, $str2) {
// 简化版相似度计算
similar_text($str1, $str2, $percent);
return $percent / 100;
}
private function handle_duplicate_errors($errors, $title, $post_id) {
$error_html = '<div class="tdp-error-notice">';
$error_html .= '<h3>标题重复警告</h3>';
$error_html .= '<p>您尝试发布的标题 "<strong>' . esc_html($title) . '</strong>" 存在以下问题:</p>';
foreach ($errors as $error) {
$error_html .= '<div class="error-type ' . $error['type'] . '">';
$error_html .= '<h4>' . $error['message'] . '</h4>';
if (!empty($error['posts'])) {
$error_html .= '<ul class="duplicate-posts">';
foreach ($error['posts'] as $post) {
$status_text = get_post_status_object($post['status'])->label;
$similarity_text = isset($post['similarity']) ? ' (相似度: ' . $post['similarity'] . '%)' : '';
$error_html .= sprintf(
'<li>%s%s - <a href="%s" target="_blank">编辑</a> | 状态: %s | 发布时间: %s</li>',
esc_html($post['title']),
$similarity_text,
esc_url($post['edit_link']),
$status_text,
$post['date']
);
}
$error_html .= '</ul>';
}
$error_html .= '</div>';
}
$error_html .= '<p>请修改标题或选择以下操作:</p>';
$error_html .= '<form method="post" id="tdp-error-form">';
$error_html .= '<input type="hidden" name="tdp_skip_check" value="0">';
$error_html .= '<button type="button" class="button" onclick="tdpSkipCheck()">仍然发布</button>';
$error_html .= ' <button type="button" class="button button-primary" onclick="location.reload()">修改标题</button>';
$error_html .= '</form>';
$error_html .= '</div>';
$error_html .= '<script>
function tdpSkipCheck() {
document.getElementById("tdp-error-form").tdp_skip_check.value = "1";
document.getElementById("publish").click();
}
</script>';
// 保存到会话
set_transient('tdp_last_error_' . get_current_user_id(), $error_html, 300);
// 添加管理通知
if (is_admin()) {
add_action('admin_notices', function() use ($error_html) {
echo '<div class="notice notice-error is-dismissible">' . $error_html . '</div>';
});
}
// 记录日志
error_log(sprintf(
'TDP: Duplicate title detected. Title: "%s", Post ID: %s, Error count: %s',
$title,
$post_id,
count($errors)
));
}
public function ajax_check_title() {
check_ajax_referer('tdp_ajax_nonce', 'nonce');
$title = isset($_POST['title']) ? sanitize_text_field($_POST['title']) : '';
$post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
$post_type = isset($_POST['post_type']) ? sanitize_text_field($_POST['post_type']) : 'post';
if (empty($title)) {
wp_send_json_error(['message' => '标题不能为空']);
}
$response = [
'exact_match' => ['found' => false, 'posts' => []],
'similar_match' => ['found' => false, 'posts' => []]
];
// 检查完全重复
if ($this->check_exact_match) {
$exact_result = $this->check_exact_duplicate($title, $post_id, $post_type);
$response['exact_match'] = $exact_result;
}
// 检查相似标题
if ($this->check_similar_match) {
$similar_result = $this->check_similar_titles($title, $post_id, $post_type);
$response['similar_match'] = [
'found' => !empty($similar_result['posts']),
'posts' => $similar_result['posts'],
'highest_similarity' => $similar_result['highest_similarity']
];
}
wp_send_json_success($response);
}
public function ajax_check_title_nopriv() {
wp_send_json_error(['message' => '未授权访问']);
}
public function enqueue_admin_scripts($hook) {
if (!in_array($hook, ['post.php', 'post-new.php', 'settings_page_tdp-settings'])) {
return;
}
wp_enqueue_script('tdp-admin-js',
plugin_dir_url(__FILE__) . 'assets/js/admin.js',
['jquery'],
'1.0.0',
true
);
wp_enqueue_style('tdp-admin-css',
plugin_dir_url(__FILE__) . 'assets/css/admin.css',
[],
'1.0.0'
);
wp_localize_script('tdp-admin-js', 'tdp_ajax', [
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('tdp_ajax_nonce'),
'checking' => '检查标题中...',
'exact_warning' => '发现完全相同的标题',
'similar_warning' => '发现相似标题',
'safe' => '标题可用'
]);
}
public function add_admin_menu() {
add_options_page(
'重复标题检查设置',
'标题查重',
'manage_options',
'tdp-settings',
[$this, 'display_settings_page']
);
}
public function register_settings() {
register_setting('tdp_settings', 'tdp_similarity_threshold', [
'type' => 'float',
'default' => 0.85,
'sanitize_callback' => 'floatval'
]);
register_setting('tdp_settings', 'tdp_check_exact', [
'type' => 'boolean',
'default' => true
]);
register_setting('tdp_settings', 'tdp_check_similar', [
'type' => 'boolean',
'default' => true
]);
register_setting('tdp_settings', 'tdp_force_check', [
'type' => 'boolean',
'default' => false
]);
}
public function display_settings_page() {
?>
<div class="wrap">
<h1>重复标题检查设置</h1>
<form method="post" action="options.php">
<?php settings_fields('tdp_settings'); ?>
<table class="form-table">
<tr>
<th scope="row">检查完全重复</th>
<td>
<label>
<input type="checkbox" name="tdp_check_exact" value="1"
<?php checked(get_option('tdp_check_exact', true)); ?>>
启用完全重复标题检查
</label>
<p class="description">检查是否存在完全相同的标题</p>
</td>
</tr>
<tr>
<th scope="row">检查相似标题</th>
<td>
<label>
<input type="checkbox" name="tdp_check_similar" value="1"
<?php checked(get_option('tdp_check_similar', true)); ?>>
启用相似标题检查
</label>
<p class="description">检查是否存在相似的标题</p>
</td>
</tr>
<tr>
<th scope="row">相似度阈值</th>
<td>
<input type="range" name="tdp_similarity_threshold"
min="0.1" max="1.0" step="0.05"
value="<?php echo esc_attr(get_option('tdp_similarity_threshold', 0.85)); ?>"
oninput="document.getElementById('thresholdValue').textContent = this.value">
<span id="thresholdValue"><?php echo get_option('tdp_similarity_threshold', 0.85); ?></span>
<p class="description">相似度达到此值将触发警告(0-1,值越小越严格)</p>
</td>
</tr>
<tr>
<th scope="row">强制检查</th>
<td>
<label>
<input type="checkbox" name="tdp_force_check" value="1"
<?php checked(get_option('tdp_force_check', false)); ?>>
强制阻止重复标题保存
</label>
<p class="description">启用后,发现重复标题将无法保存(需修改标题)</p>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
<div class="card" style="margin-top: 20px;">
<h2>批量查重工具</h2>
<p>扫描网站中所有重复标题的文章</p>
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
<input type="hidden" name="action" value="tdp_bulk_check">
<?php wp_nonce_field('tdp_bulk_check_nonce'); ?>
<button type="submit" class="button button-primary">开始批量扫描</button>
</form>
</div>
</div>
<?php
}
public function handle_bulk_check() {
check_admin_referer('tdp_bulk_check_nonce');
// 批量检查逻辑
$duplicates = $this->bulk_find_duplicates();
set_transient('tdp_bulk_results', $duplicates, 3600);
wp_redirect(admin_url('options-general.php?page=tdp-settings&tab=results'));
exit;
}
public function shortcode_title_check($atts) {
$atts = shortcode_atts([
'placeholder' => '输入标题进行检查',
'button_text' => '检查标题'
], $atts);
ob_start();
?>
<div class="tdp-frontend-checker">
<input type="text" class="tdp-title-input" placeholder="<?php echo esc_attr($atts['placeholder']); ?>">
<button class="tdp-check-button"><?php echo esc_html($atts['button_text']); ?></button>
<div class="tdp-results"></div>
</div>
<script>
jQuery(document).ready(function($) {
$('.tdp-check-button').on('click', function() {
var title = $(this).siblings('.tdp-title-input').val();
var $results = $(this).siblings('.tdp-results');
if (!title.trim()) {
$results.html('<p class="error">请输入标题</p>');
return;
}
$results.html('<p>检查中...</p>');
$.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
type: 'POST',
data: {
action: 'tdp_check_title',
title: title,
nonce: '<?php echo wp_create_nonce('tdp_ajax_nonce'); ?>'
},
success: function(response) {
if (response.success) {
var html = '<div class="tdp-result">';
if (response.data.exact_match.found) {
html += '<p class="warning">发现完全相同的标题</p>';
html += '<ul>';
response.data.exact_match.posts.forEach(function(post) {
html += '<li>' + post.title + ' (<a href="' + post.edit_link + '">查看</a>)</li>';
});
html += '</ul>';
} else if (response.data.similar_match.found) {
html += '<p class="warning">发现相似标题(相似度' + response.data.similar_match.highest_similarity + '%)</p>';
html += '<ul>';
response.data.similar_match.posts.forEach(function(post) {
html += '<li>' + post.title + ' (相似度: ' + post.similarity + '%, <a href="' + post.edit_link + '">查看</a>)</li>';
});
html += '</ul>';
} else {
html += '<p class="success">标题可用</p>';
}
html += '</div>';
$results.html(html);
}
}
});
});
});
</script>
<?php
return ob_get_clean();
}
public function register_rest_routes() {
register_rest_route('tdp/v1', '/check', [
'methods' => 'POST',
'callback' => [$this, 'rest_check_title'],
'permission_callback' => '__return_true',
'args' => [
'title' => [
'required' => true,
'validate_callback' => function($param) {
return !empty($param);
}
],
'post_id' => [
'required' => false,
'default' => 0
],
'post_type' => [
'required' => false,
'default' => 'post'
]
]
]);
}
public function rest_check_title($request) {
$title = sanitize_text_field($request['title']);
$post_id = intval($request['post_id']);
$post_type = sanitize_text_field($request['post_type']);
$result = [
'exact_duplicates' => [],
'similar_titles' => []
];
// 检查完全重复
$exact_result = $this->check_exact_duplicate($title, $post_id, $post_type);
if ($exact_result['found']) {
$result['exact_duplicates'] = $exact_result['posts'];
}
// 检查相似标题
$similar_result = $this->check_similar_titles($title, $post_id, $post_type);
if (!empty($similar_result['posts'])) {
$result['similar_titles'] = $similar_result['posts'];
}
$result['has_duplicates'] = !empty($result['exact_duplicates']) || !empty($result['similar_titles']);
return rest_ensure_response($result);
}
private function bulk_find_duplicates() {
global $wpdb;
$query = "
SELECT post_title, COUNT(*) as count,
GROUP_CONCAT(ID ORDER BY post_date DESC) as post_ids
FROM {$wpdb->posts}
WHERE post_type IN ('post', 'page')
AND post_status IN ('publish', 'pending', 'draft', 'future', 'private')
GROUP BY post_title
HAVING count > 1
ORDER BY count DESC, post_title
";
$results = $wpdb->get_results($query);
$duplicates = [];
foreach ($results as $row) {
$post_ids = explode(',', $row->post_ids);
$post_details = [];
foreach ($post_ids as $post_id) {
$post = get_post($post_id);
if ($post) {
$post_details[] = [
'id' => $post->ID,
'title' => $post->post_title,
'status' => $post->post_status,
'date' => $post->post_date,
'edit_link' => get_edit_post_link($post->ID)
];
}
}
$duplicates[] = [
'title' => $row->post_title,
'count' => $row->count,
'posts' => $post_details
];
}
return $duplicates;
}
}
// 初始化插件
function tdp_init() {
return Title_Duplicate_Preventer_2026::get_instance();
}
add_action('plugins_loaded', 'tdp_init');
// 插件激活时创建数据库表
register_activation_hook(__FILE__, function() {
global $wpdb;
$table_name = $wpdb->prefix . 'tdp_logs';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
id bigint(20) NOT NULL AUTO_INCREMENT,
post_id bigint(20) DEFAULT 0,
title varchar(255) NOT NULL,
duplicate_type varchar(20) NOT NULL,
similarity float DEFAULT 0,
matched_post_id bigint(20) DEFAULT 0,
user_id bigint(20) DEFAULT 0,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY post_id (post_id),
KEY duplicate_type (duplicate_type),
KEY created_at (created_at)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
// 设置默认选项
add_option('tdp_similarity_threshold', 0.85);
add_option('tdp_check_exact', 1);
add_option('tdp_check_similar', 1);
add_option('tdp_force_check', 0);
});
七、最佳实践与建议
1. 性能优化建议
- 索引优化:确保posts表的post_title字段有索引
- 缓存结果:频繁检查的结果应该缓存
- 分批处理:批量操作时分批进行
- 异步检查:非关键检查使用异步AJAX
- 限制范围:只检查必要的内容类型
2. 用户体验建议
- 实时反馈:输入时实时检查
- 清晰提示:明确显示重复信息
- 智能建议:提供修改建议
- 操作选项:允许强制保存
- 学习模式:记录用户习惯
3. 安全注意事项
- SQL注入防护:使用WordPress数据库API
- XSS防护:输出时转义HTML
- 权限验证:检查用户权限
- 频率限制:防止滥用检查
- 日志记录:记录重要操作
4. 2026年技术趋势
- AI智能检测:使用机器学习识别重复
- 语义分析:理解内容而不仅是文字
- 多语言支持:跨语言重复检测
- 图像识别:检测重复的图片内容
- 区块链验证:内容原创性验证
八、总结
通过本文提供的完整代码解决方案,你可以根据实际需求选择合适的重复标题防止方案。建议从基础方案开始,逐步增加高级功能。关键是要在内容质量和用户体验之间找到平衡。
推荐实施步骤:
- 从方案1开始,实现基本检查
- 添加AJAX实时检查(方案2)
- 考虑智能相似度检查(方案3)
- 定期运行批量检查(方案5)
- 根据需求选择完整插件方案
记住:技术是手段,提升内容质量才是目的。合理的重复检查可以帮助创建更好的内容,但不应过度限制创作自由。


湘公网安备43020002000238