feat: 用户被多次举报合并

This commit is contained in:
DengDai
2025-11-25 09:36:36 +08:00
parent 881188587d
commit 40feb92473
7 changed files with 120 additions and 44 deletions

View File

@@ -61,9 +61,10 @@ def create_app(config_name='default'):
app.logger.info('PT黑名单系统启动') app.logger.info('PT黑名单系统启动')
# 注册自定义过滤器 # 注册自定义过滤器
from .filters import translate_status, translate_reason from .filters import translate_status, translate_reason, translate_reasons_list
app.jinja_env.filters['translate_status'] = translate_status app.jinja_env.filters['translate_status'] = translate_status
app.jinja_env.filters['translate_reason'] = translate_reason app.jinja_env.filters['translate_reason'] = translate_reason
app.jinja_env.filters['translate_reasons_list'] = translate_reasons_list
# 注册蓝图 # 注册蓝图
from .routes import main as main_blueprint from .routes import main as main_blueprint

View File

@@ -38,3 +38,9 @@ def translate_status(status):
def translate_reason(reason): def translate_reason(reason):
"""违规原因翻译过滤器""" """违规原因翻译过滤器"""
return REASON_TRANSLATIONS.get(reason, reason) return REASON_TRANSLATIONS.get(reason, reason)
def translate_reasons_list(reasons):
"""违规原因列表翻译过滤器"""
if not reasons:
return []
return [REASON_TRANSLATIONS.get(r, r) for r in reasons]

View File

@@ -64,7 +64,6 @@ class Report(db.Model):
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
comments = db.relationship('Comment', backref='report', lazy='dynamic', cascade='all, delete-orphan') comments = db.relationship('Comment', backref='report', lazy='dynamic', cascade='all, delete-orphan')
evidences = db.relationship('Evidence', backref='report', lazy='dynamic', cascade='all, delete-orphan') evidences = db.relationship('Evidence', backref='report', lazy='dynamic', cascade='all, delete-orphan')
blacklist_entry = db.relationship('Blacklist', backref='report', uselist=False)
def __repr__(self): def __repr__(self):
return f'<Report {self.id}>' return f'<Report {self.id}>'
@@ -96,7 +95,8 @@ class Blacklist(db.Model):
normalized_email = db.Column(db.String(120), index=True) normalized_email = db.Column(db.String(120), index=True)
pt_site = db.Column(db.String(100), index=True) pt_site = db.Column(db.String(100), index=True)
uid = db.Column(db.String(50)) uid = db.Column(db.String(50))
report_id = db.Column(db.Integer, db.ForeignKey('reports.id'), unique=True) report_ids = db.Column(db.JSON, default=list, nullable=False)
reason_categories = db.Column(db.JSON, default=list, nullable=False)
status = db.Column(db.String(16), default='active', index=True) status = db.Column(db.String(16), default='active', index=True)
created_at = db.Column(db.DateTime, index=True, default=datetime.utcnow) created_at = db.Column(db.DateTime, index=True, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

View File

@@ -1,5 +1,6 @@
from flask import abort, Blueprint, render_template, request, flash, redirect, url_for, current_app from flask import abort, Blueprint, render_template, request, flash, redirect, url_for, current_app
from sqlalchemy import or_ from sqlalchemy import or_
from sqlalchemy.orm.attributes import flag_modified
from flask_login import login_required, current_user from flask_login import login_required, current_user
from app import db from app import db
from app.forms import SearchForm, ReportForm, UpdateUserForm, CommentForm, RevokeForm, AppealForm, AppealMessageForm, PartnerSiteForm from app.forms import SearchForm, ReportForm, UpdateUserForm, CommentForm, RevokeForm, AppealForm, AppealMessageForm, PartnerSiteForm
@@ -24,13 +25,12 @@ def index():
search_term = form.search_term.data search_term = form.search_term.data
normalized_email = normalize_email(search_term) normalized_email = normalize_email(search_term)
search_result = Blacklist.query.join(Report).filter( search_result = Blacklist.query.filter(
or_( or_(
Blacklist.normalized_email == normalized_email, Blacklist.normalized_email == normalized_email,
Blacklist.username == search_term Blacklist.username == search_term
), ),
Blacklist.status == 'active', Blacklist.status == 'active'
Report.status == 'approved'
).first() ).first()
if search_result: if search_result:
@@ -46,6 +46,9 @@ def index():
@login_required @login_required
def create_report(): def create_report():
"""创建新举报""" """创建新举报"""
if current_user.status != 'active':
flash('您的账户尚未激活,无法提交举报。请等待管理员审核。', 'warning')
return redirect(url_for('main.index'))
form = ReportForm() form = ReportForm()
active_sites = PartnerSite.query.filter_by(is_active=True).order_by(PartnerSite.name).all() active_sites = PartnerSite.query.filter_by(is_active=True).order_by(PartnerSite.name).all()
form.reported_pt_site.choices = [(site.name, site.name) for site in active_sites] form.reported_pt_site.choices = [(site.name, site.name) for site in active_sites]
@@ -170,17 +173,33 @@ def process_report(report_id):
if action == 'confirm': if action == 'confirm':
report.status = 'approved' report.status = 'approved'
existing_blacklist = Blacklist.query.filter_by(report_id=report.id).first() normalized = normalize_email(report.reported_email)
existing_blacklist = Blacklist.query.filter_by(normalized_email=normalized, status='active').first()
if not existing_blacklist: if not existing_blacklist:
new_blacklist_entry = Blacklist( new_blacklist_entry = Blacklist(
email=report.reported_email, email=report.reported_email,
normalized_email=normalize_email(report.reported_email), normalized_email=normalized,
pt_site=report.reported_pt_site, pt_site=report.reported_pt_site,
uid=report.reported_username, uid=report.reported_username,
report_id=report.id, report_ids=[report.id],
reason_categories=[report.reason_category],
username=report.reported_username or None username=report.reported_username or None
) )
db.session.add(new_blacklist_entry) db.session.add(new_blacklist_entry)
current_app.logger.info(f'举报批准: #{report.id} - {report.reported_email} by {current_user.username}')
flash('举报已批准,并已将相关信息添加到黑名单。', 'success')
else:
if report.reason_category not in existing_blacklist.reason_categories:
existing_blacklist.reason_categories.append(report.reason_category)
existing_blacklist.report_ids.append(report.id)
flag_modified(existing_blacklist, 'reason_categories')
flag_modified(existing_blacklist, 'report_ids')
current_app.logger.info(f'举报合并: #{report.id} 合并到黑名单#{existing_blacklist.id} - 新增原因: {report.reason_category}')
flash(f'举报已批准并合并到现有黑名单记录(新增违规原因:{report.reason_category})。', 'success')
else:
current_app.logger.info(f'举报批准: #{report.id} - 相同原因已存在,不合并')
flash('举报已批准。该用户已有相同违规原因的记录,未进行合并。', 'info')
other_pending = Report.query.filter( other_pending = Report.query.filter(
Report.reported_email == report.reported_email, Report.reported_email == report.reported_email,
@@ -188,8 +207,16 @@ def process_report(report_id):
Report.status == 'pending' Report.status == 'pending'
).all() ).all()
merged_count = 0
for other_report in other_pending: for other_report in other_pending:
other_report.status = 'approved' other_report.status = 'approved'
bl = existing_blacklist or new_blacklist_entry
if other_report.reason_category not in bl.reason_categories:
bl.reason_categories.append(other_report.reason_category)
bl.report_ids.append(other_report.id)
flag_modified(bl, 'reason_categories')
flag_modified(bl, 'report_ids')
merged_count += 1
comment = Comment( comment = Comment(
body=f'该举报已自动批准(关联举报 #{report.id} 已确认违规)', body=f'该举报已自动批准(关联举报 #{report.id} 已确认违规)',
report=other_report, report=other_report,
@@ -197,14 +224,9 @@ def process_report(report_id):
) )
db.session.add(comment) db.session.add(comment)
current_app.logger.info(f'举报批准: #{report.id} - {report.reported_email} by {current_user.username}')
if other_pending: if other_pending:
current_app.logger.info(f'自动批准关联举报: {len(other_pending)}') current_app.logger.info(f'自动批准关联举报: {len(other_pending)},合并{merged_count}个不同原因')
flash(f'举报已批准,并已将相关信息添加到黑名单。同时自动处理了 {len(other_pending)} 个相关举报。', 'success') flash(f'同时自动处理了 {len(other_pending)} 个相关举报(其中 {merged_count} 个不同原因已合并)', 'info')
else:
flash('举报已批准,并已将相关信息添加到黑名单。', 'success')
else:
flash('举报状态已更新为"批准"。该举报已在黑名单中,无需重复添加。', 'info')
elif action == 'invalidate': elif action == 'invalidate':
report.status = 'rejected' report.status = 'rejected'
current_app.logger.info(f'举报驳回: #{report.id} by {current_user.username}') current_app.logger.info(f'举报驳回: #{report.id} by {current_user.username}')
@@ -224,9 +246,23 @@ def revoke_report(report_id):
return redirect(url_for('main.report_detail', report_id=report.id)) return redirect(url_for('main.report_detail', report_id=report.id))
form = RevokeForm() form = RevokeForm()
if form.validate_on_submit(): if form.validate_on_submit():
blacklist_entry = Blacklist.query.filter_by(report_id=report.id).first() normalized = normalize_email(report.reported_email)
if blacklist_entry: blacklist_entry = Blacklist.query.filter_by(normalized_email=normalized, status='active').first()
if blacklist_entry and report.id in blacklist_entry.report_ids:
blacklist_entry.report_ids.remove(report.id)
if report.reason_category in blacklist_entry.reason_categories:
blacklist_entry.reason_categories.remove(report.reason_category)
flag_modified(blacklist_entry, 'report_ids')
flag_modified(blacklist_entry, 'reason_categories')
if len(blacklist_entry.report_ids) == 0:
db.session.delete(blacklist_entry) db.session.delete(blacklist_entry)
current_app.logger.warning(f'举报撤销: #{report.id} - 黑名单记录已删除')
flash('举报已成功撤销,并已从黑名单中移除。', 'success')
else:
current_app.logger.warning(f'举报撤销: #{report.id} - 从黑名单中移除该举报')
flash(f'举报已成功撤销,已从黑名单中移除该违规原因(剩余 {len(blacklist_entry.report_ids)} 个举报)。', 'success')
report.status = 'revoked' report.status = 'revoked'
revocation_comment = Comment( revocation_comment = Comment(
@@ -237,7 +273,6 @@ def revoke_report(report_id):
db.session.add(revocation_comment) db.session.add(revocation_comment)
db.session.commit() db.session.commit()
current_app.logger.warning(f'举报撤销: #{report.id} by {current_user.username} - 理由: {form.reason.data[:50]}') current_app.logger.warning(f'举报撤销: #{report.id} by {current_user.username} - 理由: {form.reason.data[:50]}')
flash('举报已成功撤销,并已从黑名单中移除。', 'success')
else: else:
flash('撤销失败:' + ' '.join(form.reason.errors), 'danger') flash('撤销失败:' + ' '.join(form.reason.errors), 'danger')
return redirect(url_for('main.report_detail', report_id=report.id)) return redirect(url_for('main.report_detail', report_id=report.id))
@@ -398,9 +433,11 @@ def decide_appeal(appeal_id):
blacklist_entry = appeal.blacklist_entry blacklist_entry = appeal.blacklist_entry
blacklist_entry.status = 'revoked' blacklist_entry.status = 'revoked'
appeal.status = 'approved' appeal.status = 'approved'
if blacklist_entry.report: for report_id in blacklist_entry.report_ids:
blacklist_entry.report.status = 'overturned' report = Report.query.get(report_id)
db.session.add(blacklist_entry.report) if report:
report.status = 'overturned'
db.session.add(report)
db.session.add(blacklist_entry) db.session.add(blacklist_entry)
db.session.add(appeal) db.session.add(appeal)
db.session.commit() db.session.commit()

View File

@@ -24,6 +24,18 @@
{% if appeal.blacklist_entry %} {% if appeal.blacklist_entry %}
<p class="mb-0"><strong>站点:</strong> {{ appeal.blacklist_entry.pt_site }}</p> <p class="mb-0"><strong>站点:</strong> {{ appeal.blacklist_entry.pt_site }}</p>
<p class="mb-0"><strong>UID:</strong> {{ appeal.blacklist_entry.uid }}</p> <p class="mb-0"><strong>UID:</strong> {{ appeal.blacklist_entry.uid }}</p>
<p class="mb-1"><strong>违规原因:</strong></p>
{% if appeal.blacklist_entry.reason_categories and appeal.blacklist_entry.reason_categories|length > 0 %}
<ul class="mb-0">
{% for reason in appeal.blacklist_entry.reason_categories %}
<li>{{ reason | translate_reason }}</li>
{% endfor %}
</ul>
{% elif appeal.blacklist_entry.report %}
<p class="mb-0">{{ appeal.blacklist_entry.report.reason_category | translate_reason }}</p>
{% else %}
<p class="mb-0 text-muted">未知</p>
{% endif %}
{% else %} {% else %}
<p class="mb-0 text-muted">黑名单记录已删除</p> <p class="mb-0 text-muted">黑名单记录已删除</p>
{% endif %} {% endif %}

View File

@@ -14,9 +14,20 @@
<li class="list-group-item"><strong>UID:</strong> {{ entry.uid }}</li> <li class="list-group-item"><strong>UID:</strong> {{ entry.uid }}</li>
<li class="list-group-item"><strong>邮箱:</strong> {{ entry.email }}</li> <li class="list-group-item"><strong>邮箱:</strong> {{ entry.email }}</li>
<li class="list-group-item"><strong>站点:</strong> {{ entry.pt_site }}</li> <li class="list-group-item"><strong>站点:</strong> {{ entry.pt_site }}</li>
{% if entry.report %} <li class="list-group-item">
<li class="list-group-item"><strong>违规原因:</strong> {{ entry.report.reason_category | translate_reason }}</li> <strong>违规原因:</strong>
{% if entry.reason_categories and entry.reason_categories|length > 0 %}
<ul class="mb-0 mt-1">
{% for reason in entry.reason_categories %}
<li>{{ reason | translate_reason }}</li>
{% endfor %}
</ul>
{% elif entry.report %}
{{ entry.report.reason_category | translate_reason }}
{% else %}
未知
{% endif %} {% endif %}
</li>
</ul> </ul>
<form method="POST" novalidate> <form method="POST" novalidate>

View File

@@ -45,9 +45,18 @@
</div> </div>
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
<li class="list-group-item"><strong>违规站点:</strong> {{ search_result.pt_site }}</li> <li class="list-group-item"><strong>违规站点:</strong> {{ search_result.pt_site }}</li>
{% if search_result.report %} <li class="list-group-item">
<li class="list-group-item"><strong>违规原因:</strong> {{ search_result.report.reason_category | translate_reason }}</li> <strong>违规原因:</strong>
{% if search_result.reason_categories and search_result.reason_categories|length > 0 %}
<ul class="mb-0 mt-1">
{% for reason in search_result.reason_categories %}
<li>{{ reason | translate_reason }}</li>
{% endfor %}
</ul>
{% else %}
未知
{% endif %} {% endif %}
</li>
<li class="list-group-item"><strong>记录时间:</strong> {{ search_result.created_at.strftime('%Y-%m-%d') }}</li> <li class="list-group-item"><strong>记录时间:</strong> {{ search_result.created_at.strftime('%Y-%m-%d') }}</li>
</ul> </ul>
<p class="text-muted small mt-3">为保护隐私,仅展示必要的脱敏信息。具体违规描述不对外公开。</p> <p class="text-muted small mt-3">为保护隐私,仅展示必要的脱敏信息。具体违规描述不对外公开。</p>