feat(comments): 添加 IP 黑名单功能

- 在后台管理界面添加 IP 黑名单设置项
- 支持在评论列表中直接屏蔽 IP
- 实现 API 层 IP 校验逻辑,阻止黑名单 IP 发表评论
- 新增 IP 黑名单相关数据库字段和接口
This commit is contained in:
anghunk
2026-01-21 09:55:10 +08:00
parent bf28ce2e2a
commit 49962db38d
5 changed files with 124 additions and 5 deletions

View File

@@ -43,6 +43,7 @@ export type CommentSettingsResponse = {
adminKey?: string | null;
adminKeySet?: boolean;
requireReview?: boolean;
blockedIps?: string[];
};
export type EmailNotifySettingsResponse = {
@@ -137,10 +138,15 @@ export function saveCommentSettings(data: {
allowedDomains?: string[];
adminKey?: string;
requireReview?: boolean;
blockedIps?: string[];
}): Promise<{ message: string }> {
return put<{ message: string }>('/admin/settings/comments', data);
}
export function blockIp(ip: string): Promise<{ message: string }> {
return post<{ message: string }>('/admin/comments/block-ip', { ip });
}
export function exportComments(): Promise<any[]> {
return get<any[]>('/admin/comments/export');
}

View File

@@ -38,6 +38,9 @@
<div class="cell-author-name">{{ item.name }}</div>
<div class="cell-author-email">{{ item.email }}</div>
<span class="cell-time">{{ formatDate(item.created) }}</span>
<div v-if="item.ipAddress" class="cell-author-ip">
<span class="cell-ip-text" @click="handleBlockIp(item)" title="屏蔽该 IP">{{ item.ipAddress }}</span>
</div>
</div>
</div>
</div>
@@ -176,6 +179,7 @@ import {
fetchComments,
deleteComment,
updateCommentStatus,
blockIp,
} from "../api/admin";
const comments = ref<CommentItem[]>([]);
@@ -292,6 +296,21 @@ async function removeComment(item: CommentItem) {
}
}
async function handleBlockIp(item: CommentItem) {
if (!item.ipAddress) {
return;
}
if (!window.confirm(`确认将 IP ${item.ipAddress} 加入黑名单吗?`)) {
return;
}
try {
const res = await blockIp(item.ipAddress);
window.alert(res.message || "已加入 IP 黑名单");
} catch (e: any) {
error.value = e.message || "屏蔽 IP 失败";
}
}
onMounted(() => {
loadComments();
});
@@ -621,4 +640,12 @@ onMounted(() => {
border: 1px solid #d0d7de;
font-size: 12px;
}
.cell-ip-text {
cursor: pointer;
}
.cell-ip-text:hover {
text-decoration: underline;
}
</style>

View File

@@ -48,6 +48,15 @@
placeholder="例如: example.com, test.com"
></textarea>
</div>
<div class="form-item">
<label class="form-label">IP 黑名单多个 IP 用逗号或换行分隔留空则不限制</label>
<textarea
v-model="blockedIps"
class="form-input"
rows="3"
placeholder="例如: 1.1.1.1, 2.2.2.2"
></textarea>
</div>
<div class="form-item">
<label class="form-label">管理员评论密钥</label>
<div class="form-hint" style="margin-bottom: 4px;">
@@ -305,6 +314,7 @@ const commentAdminBadge = ref("");
const avatarPrefix = ref("");
const commentAdminEnabled = ref(false);
const allowedDomains = ref("");
const blockedIps = ref("");
const commentAdminKey = ref("");
const adminKeySet = ref(false);
const requireReview = ref(false);
@@ -365,6 +375,9 @@ async function load() {
allowedDomains.value = commentRes.allowedDomains
? commentRes.allowedDomains.join(", ")
: "";
blockedIps.value = commentRes.blockedIps
? commentRes.blockedIps.join(", ")
: "";
commentAdminKey.value = commentRes.adminKey || "";
adminKeySet.value = !!commentRes.adminKeySet;
requireReview.value = !!commentRes.requireReview;
@@ -498,6 +511,10 @@ async function saveComment() {
.filter(Boolean),
adminKey: commentAdminKey.value || undefined,
requireReview: requireReview.value,
blockedIps: blockedIps.value
.split(/[,\n]/)
.map((d) => d.trim())
.filter(Boolean),
});
showToast(res.message || "保存成功", "success");