mirror of
https://github.com/GuDong2003/xianyu-auto-reply-fix.git
synced 2026-08-28 17:40:45 +08:00
feat(黑名单): 新增管理功能
This commit is contained in:
@@ -3359,6 +3359,81 @@ class XianyuLive:
|
||||
delay_task = asyncio.create_task(self._delayed_lock_release(lock_key, delay_minutes=delay_minutes))
|
||||
self._lock_hold_info[lock_key]['task'] = delay_task
|
||||
|
||||
def _resolve_blacklist_user_id(self) -> Optional[int]:
|
||||
"""获取当前账号归属用户,用于黑名单隔离。"""
|
||||
if self.user_id:
|
||||
try:
|
||||
return int(self.user_id)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
try:
|
||||
cookie_details = db_manager.get_cookie_details(self.cookie_id) or {}
|
||||
user_id = cookie_details.get('user_id')
|
||||
return int(user_id) if user_id else None
|
||||
except Exception as e:
|
||||
logger.warning(f"【{self.cookie_id}】解析黑名单用户归属失败: {self._safe_str(e)}")
|
||||
return None
|
||||
|
||||
def _format_blacklist_block_reason(self, hit: Dict[str, Any], action: str = '自动动作') -> str:
|
||||
scope_label = {
|
||||
'item': '商品级',
|
||||
'account': '账号级',
|
||||
'user': '用户级',
|
||||
}.get((hit or {}).get('scope'), (hit or {}).get('scope') or '未知级别')
|
||||
reason = str((hit or {}).get('reason') or '').strip()
|
||||
reason_part = f",原因:{reason}" if reason else ''
|
||||
buyer_id = (hit or {}).get('buyer_id') or '未知买家'
|
||||
return f"买家 {buyer_id} 命中个人黑名单 scope={scope_label}{reason_part},跳过{action}"
|
||||
|
||||
def _check_buyer_blacklist_for_action(self, buyer_id: str = None, item_id: str = None,
|
||||
order_id: str = None, buyer_nick: str = None,
|
||||
action: str = '自动动作', channel: str = 'auto',
|
||||
log_delivery: bool = False) -> Optional[Dict[str, Any]]:
|
||||
"""检查买家黑名单,命中时可记录发货跳过日志。"""
|
||||
normalized_buyer_id = str(buyer_id or '').strip()
|
||||
if not normalized_buyer_id:
|
||||
return None
|
||||
|
||||
try:
|
||||
user_id = self._resolve_blacklist_user_id()
|
||||
if not user_id:
|
||||
return None
|
||||
|
||||
hit = db_manager.is_buyer_blacklisted(
|
||||
user_id=user_id,
|
||||
buyer_id=normalized_buyer_id,
|
||||
cookie_id=self.cookie_id,
|
||||
item_id=str(item_id or '').strip() or None,
|
||||
)
|
||||
if not hit:
|
||||
return None
|
||||
|
||||
block_reason = self._format_blacklist_block_reason(hit, action=action)
|
||||
logger.warning(
|
||||
f"【{self.cookie_id}】买家 {normalized_buyer_id} 命中个人黑名单,"
|
||||
f"scope={hit.get('scope')}, item_id={item_id or ''}, order_id={order_id or ''},跳过{action}"
|
||||
)
|
||||
|
||||
if log_delivery:
|
||||
self._record_delivery_log(
|
||||
order_id=order_id,
|
||||
item_id=item_id,
|
||||
buyer_id=normalized_buyer_id,
|
||||
buyer_nick=buyer_nick,
|
||||
status='skipped',
|
||||
reason=block_reason,
|
||||
channel=channel,
|
||||
rule_meta={'match_mode': 'blacklist'},
|
||||
)
|
||||
return hit
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"【{self.cookie_id}】检查买家黑名单失败: buyer_id={normalized_buyer_id}, "
|
||||
f"item_id={item_id or ''}, error={self._safe_str(e)}"
|
||||
)
|
||||
return None
|
||||
|
||||
def _record_delivery_log(self, order_id: str = None, item_id: str = None, buyer_id: str = None,
|
||||
buyer_nick: str = None, status: str = 'failed', reason: str = None,
|
||||
channel: str = 'auto', rule_meta: dict = None):
|
||||
@@ -4513,6 +4588,16 @@ class XianyuLive:
|
||||
)
|
||||
return
|
||||
|
||||
if self._check_buyer_blacklist_for_action(
|
||||
buyer_id=user_id,
|
||||
item_id=item_id,
|
||||
order_id=order_id,
|
||||
action='自动发货',
|
||||
channel='auto',
|
||||
log_delivery=True,
|
||||
):
|
||||
return
|
||||
|
||||
# 检查订单是否已发货
|
||||
if not self.can_auto_delivery(order_id):
|
||||
logger.info(f'[{msg_time}] 【{self.cookie_id}】[{msg_id}] 订单 {order_id} 在冷却期内,跳过发货')
|
||||
@@ -4863,6 +4948,16 @@ class XianyuLive:
|
||||
)
|
||||
return
|
||||
|
||||
if self._check_buyer_blacklist_for_action(
|
||||
buyer_id=send_user_id,
|
||||
item_id=item_id,
|
||||
buyer_nick=send_user_name,
|
||||
action='自动发货',
|
||||
channel='auto',
|
||||
log_delivery=True,
|
||||
):
|
||||
return
|
||||
|
||||
# 提取订单ID(传递原始消息数据以便在解密消息中找不到时进行备用搜索)
|
||||
order_id = self._extract_order_id(message, message_data)
|
||||
|
||||
@@ -5039,6 +5134,17 @@ class XianyuLive:
|
||||
item_id = existing_item_id
|
||||
logger.info(f'[{msg_time}] 【{self.cookie_id}】订单一致性校验补全商品ID: {item_id}')
|
||||
|
||||
if self._check_buyer_blacklist_for_action(
|
||||
buyer_id=send_user_id,
|
||||
item_id=item_id,
|
||||
order_id=order_id,
|
||||
buyer_nick=send_user_name,
|
||||
action='自动发货',
|
||||
channel='auto',
|
||||
log_delivery=True,
|
||||
):
|
||||
return
|
||||
|
||||
logger.info(f'[{msg_time}] 【{self.cookie_id}】提取到订单ID: {order_id},将在自动发货时处理确认发货')
|
||||
|
||||
# 使用订单ID作为锁的键
|
||||
@@ -9062,6 +9168,15 @@ class XianyuLive:
|
||||
async def get_ai_reply(self, send_user_name: str, send_user_id: str, send_message: str, item_id: str, chat_id: str):
|
||||
"""获取AI回复"""
|
||||
try:
|
||||
if self._check_buyer_blacklist_for_action(
|
||||
buyer_id=send_user_id,
|
||||
item_id=item_id,
|
||||
buyer_nick=send_user_name,
|
||||
action='AI回复',
|
||||
log_delivery=False,
|
||||
):
|
||||
return None
|
||||
|
||||
from ai_reply_engine import ai_reply_engine
|
||||
|
||||
# 检查是否启用AI回复
|
||||
@@ -14636,6 +14751,16 @@ class XianyuLive:
|
||||
logger.info(f"[{msg_time}] 【{self.cookie_id}】【系统】chat_id {chat_id} 自动回复已暂停,剩余时间: {remaining_minutes}分{remaining_seconds}秒")
|
||||
return
|
||||
|
||||
blacklist_hit = self._check_buyer_blacklist_for_action(
|
||||
buyer_id=send_user_id,
|
||||
item_id=item_id,
|
||||
buyer_nick=send_user_name,
|
||||
action='自动回复',
|
||||
log_delivery=False,
|
||||
)
|
||||
if blacklist_hit:
|
||||
return
|
||||
|
||||
reply = None
|
||||
reply_source = None
|
||||
|
||||
@@ -15393,6 +15518,18 @@ class XianyuLive:
|
||||
|
||||
# 继续执行亦凡API调用(带账号)
|
||||
try:
|
||||
if self._check_buyer_blacklist_for_action(
|
||||
buyer_id=send_user_id,
|
||||
item_id=item_id_saved,
|
||||
order_id=order_id_saved,
|
||||
buyer_nick=send_user_name,
|
||||
action='亦凡账号确认自动发货',
|
||||
channel='auto',
|
||||
log_delivery=True,
|
||||
):
|
||||
logger.info(f"【{self.cookie_id}】[{msg_id}] 亦凡账号确认发货被黑名单拦截")
|
||||
return
|
||||
|
||||
# 直接调用亦凡API下单
|
||||
delivery_content = await self._call_yifan_api_with_account(
|
||||
rule, account, order_id_saved, item_id_saved, send_user_id, chat_id
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
"""Blacklist service helpers for personal/platform blacklist flows."""
|
||||
|
||||
import io
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import pandas as pd
|
||||
from loguru import logger
|
||||
|
||||
from db_manager import db_manager
|
||||
|
||||
|
||||
PERSONAL_BLACKLIST_EXPORT_COLUMNS = [
|
||||
'账号ID',
|
||||
'买家ID',
|
||||
'买家昵称',
|
||||
'商品ID',
|
||||
'拉黑原因',
|
||||
'是否启用',
|
||||
]
|
||||
|
||||
|
||||
class BlacklistService:
|
||||
def __init__(self, db=db_manager):
|
||||
self.db = db
|
||||
|
||||
def _normalize_bool(self, value: Any, default: bool = True) -> bool:
|
||||
if value is None:
|
||||
return default
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, (int, float)):
|
||||
return value != 0
|
||||
text = str(value).strip().lower()
|
||||
if text in {'1', 'true', 'yes', 'y', 'on', '启用', '是', '开启'}:
|
||||
return True
|
||||
if text in {'0', 'false', 'no', 'n', 'off', '禁用', '否', '关闭'}:
|
||||
return False
|
||||
return default
|
||||
|
||||
def _clean_text(self, value: Any) -> str:
|
||||
if value is None:
|
||||
return ''
|
||||
if isinstance(value, float) and pd.isna(value):
|
||||
return ''
|
||||
return str(value).strip()
|
||||
|
||||
def create_personal(
|
||||
self,
|
||||
user_id: int,
|
||||
buyer_ids: Any,
|
||||
cookie_id: Optional[str] = None,
|
||||
item_id: Optional[str] = None,
|
||||
reason: str = '',
|
||||
is_enabled: bool = True,
|
||||
buyer_nick: str = '',
|
||||
) -> Dict[str, Any]:
|
||||
return self.db.create_personal_blacklist(
|
||||
user_id=user_id,
|
||||
buyer_ids=buyer_ids,
|
||||
cookie_id=cookie_id,
|
||||
item_id=item_id,
|
||||
reason=reason,
|
||||
is_enabled=is_enabled,
|
||||
buyer_nick=buyer_nick,
|
||||
)
|
||||
|
||||
def list_personal(
|
||||
self,
|
||||
user_id: int,
|
||||
buyer_id: Optional[str] = None,
|
||||
buyer_nick: Optional[str] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> Dict[str, Any]:
|
||||
return self.db.list_personal_blacklist(
|
||||
user_id=user_id,
|
||||
buyer_id=buyer_id,
|
||||
buyer_nick=buyer_nick,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
def delete_personal(self, record_id: int, user_id: int) -> bool:
|
||||
return self.db.delete_personal_blacklist(record_id, user_id)
|
||||
|
||||
def batch_delete_personal(self, ids: List[int], user_id: int) -> int:
|
||||
return self.db.batch_delete_personal_blacklist(ids, user_id)
|
||||
|
||||
def toggle_personal(self, record_id: int, user_id: int, is_enabled: bool) -> bool:
|
||||
return self.db.toggle_personal_blacklist(record_id, user_id, is_enabled)
|
||||
|
||||
def list_platform(self, user_id: int, page: int = 1, page_size: int = 20) -> Dict[str, Any]:
|
||||
return self.db.list_platform_blacklist(user_id=user_id, page=page, page_size=page_size)
|
||||
|
||||
def is_buyer_blacklisted(
|
||||
self,
|
||||
user_id: int,
|
||||
buyer_id: str,
|
||||
cookie_id: Optional[str] = None,
|
||||
item_id: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
return self.db.is_buyer_blacklisted(
|
||||
user_id=user_id,
|
||||
buyer_id=buyer_id,
|
||||
cookie_id=cookie_id,
|
||||
item_id=item_id,
|
||||
)
|
||||
|
||||
def is_buyer_blacklisted_by_cookie(
|
||||
self,
|
||||
cookie_id: str,
|
||||
buyer_id: str,
|
||||
item_id: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
cookie_details = self.db.get_cookie_details(cookie_id) if cookie_id else None
|
||||
user_id = cookie_details.get('user_id') if cookie_details else None
|
||||
if not user_id:
|
||||
return None
|
||||
return self.is_buyer_blacklisted(
|
||||
user_id=user_id,
|
||||
buyer_id=buyer_id,
|
||||
cookie_id=cookie_id,
|
||||
item_id=item_id,
|
||||
)
|
||||
|
||||
def export_personal_xlsx(self, user_id: int) -> bytes:
|
||||
records = self.db.list_personal_blacklist(user_id=user_id, page=1, page_size=100000).get('data', [])
|
||||
rows = []
|
||||
for record in records:
|
||||
rows.append({
|
||||
'账号ID': record.get('cookie_id') or '',
|
||||
'买家ID': record.get('buyer_id') or '',
|
||||
'买家昵称': record.get('buyer_nick') or '',
|
||||
'商品ID': record.get('item_id') or '',
|
||||
'拉黑原因': record.get('reason') or '',
|
||||
'是否启用': '是' if record.get('is_enabled') else '否',
|
||||
})
|
||||
|
||||
df = pd.DataFrame(rows, columns=PERSONAL_BLACKLIST_EXPORT_COLUMNS)
|
||||
output = io.BytesIO()
|
||||
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||||
df.to_excel(writer, sheet_name='个人黑名单', index=False)
|
||||
output.seek(0)
|
||||
return output.getvalue()
|
||||
|
||||
def import_personal_xlsx(self, user_id: int, file_bytes: bytes) -> Dict[str, Any]:
|
||||
df = pd.read_excel(io.BytesIO(file_bytes), engine='openpyxl')
|
||||
missing_columns = [col for col in ['买家ID'] if col not in df.columns]
|
||||
if missing_columns:
|
||||
raise ValueError(f"缺少必需表头: {', '.join(missing_columns)}")
|
||||
|
||||
total_rows = 0
|
||||
created = 0
|
||||
skipped = 0
|
||||
errors = []
|
||||
records = []
|
||||
|
||||
for row_index, row in df.iterrows():
|
||||
total_rows += 1
|
||||
buyer_id = self._clean_text(row.get('买家ID'))
|
||||
if not buyer_id:
|
||||
skipped += 1
|
||||
errors.append(f"第 {row_index + 2} 行缺少买家ID,已跳过")
|
||||
continue
|
||||
|
||||
try:
|
||||
result = self.create_personal(
|
||||
user_id=user_id,
|
||||
buyer_ids=[buyer_id],
|
||||
cookie_id=self._clean_text(row.get('账号ID')) or None,
|
||||
item_id=self._clean_text(row.get('商品ID')) or None,
|
||||
reason=self._clean_text(row.get('拉黑原因')),
|
||||
is_enabled=self._normalize_bool(row.get('是否启用'), True),
|
||||
buyer_nick=self._clean_text(row.get('买家昵称')),
|
||||
)
|
||||
created += int(result.get('created') or 0)
|
||||
skipped += int(result.get('skipped') or 0)
|
||||
records.extend(result.get('records') or [])
|
||||
except Exception as exc:
|
||||
logger.warning(f"导入个人黑名单第 {row_index + 2} 行失败: {exc}")
|
||||
skipped += 1
|
||||
errors.append(f"第 {row_index + 2} 行导入失败: {exc}")
|
||||
|
||||
return {
|
||||
'total': total_rows,
|
||||
'created': created,
|
||||
'skipped': skipped,
|
||||
'records': records,
|
||||
'errors': errors,
|
||||
}
|
||||
|
||||
|
||||
blacklist_service = BlacklistService()
|
||||
+353
@@ -337,6 +337,40 @@ class DBManager:
|
||||
''')
|
||||
|
||||
|
||||
# 创建个人黑名单表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS xy_personal_blacklist (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
cookie_id TEXT,
|
||||
buyer_id TEXT NOT NULL,
|
||||
buyer_nick TEXT DEFAULT '',
|
||||
item_id TEXT,
|
||||
reason TEXT DEFAULT '',
|
||||
is_enabled INTEGER DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_xy_personal_blacklist_user_buyer ON xy_personal_blacklist(user_id, buyer_id)")
|
||||
self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_xy_personal_blacklist_scope ON xy_personal_blacklist(user_id, buyer_id, cookie_id, item_id, is_enabled)")
|
||||
|
||||
# 创建平台黑名单表(预留平台同步能力)
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS xy_platform_blacklist (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
buyer_id TEXT NOT NULL,
|
||||
buyer_nick TEXT DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
self._execute_sql(cursor, "CREATE INDEX IF NOT EXISTS idx_xy_platform_blacklist_user_buyer ON xy_platform_blacklist(user_id, buyer_id)")
|
||||
|
||||
# 创建keywords表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS keywords (
|
||||
@@ -10487,6 +10521,325 @@ Cookie数量: {cookie_count}
|
||||
logger.error(f"获取全量会话列表失败: {e}")
|
||||
return []
|
||||
|
||||
def _normalize_blacklist_scope_value(self, value: Any) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
text = str(value).strip()
|
||||
return text or None
|
||||
|
||||
def _normalize_blacklist_buyer_ids(self, buyer_ids: Any) -> List[str]:
|
||||
if buyer_ids is None:
|
||||
return []
|
||||
if isinstance(buyer_ids, str):
|
||||
raw_values = re.split(r'[\n,,]+', buyer_ids)
|
||||
else:
|
||||
raw_values = list(buyer_ids)
|
||||
|
||||
normalized = []
|
||||
seen = set()
|
||||
for raw_value in raw_values:
|
||||
buyer_id = str(raw_value or '').strip()
|
||||
if not buyer_id or buyer_id in seen:
|
||||
continue
|
||||
normalized.append(buyer_id)
|
||||
seen.add(buyer_id)
|
||||
return normalized
|
||||
|
||||
def _personal_blacklist_row_to_dict(self, row: tuple, columns: List[str]) -> Dict[str, Any]:
|
||||
record = dict(zip(columns, row))
|
||||
record['is_enabled'] = bool(record.get('is_enabled'))
|
||||
scope = 'user'
|
||||
if self._normalize_blacklist_scope_value(record.get('item_id')):
|
||||
scope = 'item'
|
||||
elif self._normalize_blacklist_scope_value(record.get('cookie_id')):
|
||||
scope = 'account'
|
||||
record['scope'] = scope
|
||||
return record
|
||||
|
||||
def create_personal_blacklist(
|
||||
self,
|
||||
user_id: int,
|
||||
buyer_ids: List[str],
|
||||
cookie_id: str = None,
|
||||
item_id: str = None,
|
||||
reason: str = "",
|
||||
is_enabled: bool = True,
|
||||
buyer_nick: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
"""创建个人黑名单记录,重复 scope 会跳过。"""
|
||||
normalized_buyer_ids = self._normalize_blacklist_buyer_ids(buyer_ids)
|
||||
normalized_cookie_id = self._normalize_blacklist_scope_value(cookie_id)
|
||||
normalized_item_id = self._normalize_blacklist_scope_value(item_id)
|
||||
normalized_reason = str(reason or '').strip()
|
||||
normalized_buyer_nick = str(buyer_nick or '').strip()
|
||||
|
||||
result = {
|
||||
'created': 0,
|
||||
'skipped': 0,
|
||||
'records': [],
|
||||
'skipped_buyer_ids': [],
|
||||
}
|
||||
if not user_id or not normalized_buyer_ids:
|
||||
return result
|
||||
|
||||
with self.lock:
|
||||
cursor = self.conn.cursor()
|
||||
try:
|
||||
if normalized_cookie_id:
|
||||
self._execute_sql(cursor, "SELECT user_id FROM cookies WHERE id = ?", (normalized_cookie_id,))
|
||||
cookie_owner = cursor.fetchone()
|
||||
if not cookie_owner or int(cookie_owner[0]) != int(user_id):
|
||||
result['skipped'] = len(normalized_buyer_ids)
|
||||
result['skipped_buyer_ids'] = normalized_buyer_ids
|
||||
return result
|
||||
|
||||
for buyer_id in normalized_buyer_ids:
|
||||
self._execute_sql(cursor, """
|
||||
SELECT id FROM xy_personal_blacklist
|
||||
WHERE user_id = ?
|
||||
AND buyer_id = ?
|
||||
AND COALESCE(cookie_id, '') = ?
|
||||
AND COALESCE(item_id, '') = ?
|
||||
LIMIT 1
|
||||
""", (
|
||||
user_id,
|
||||
buyer_id,
|
||||
normalized_cookie_id or '',
|
||||
normalized_item_id or '',
|
||||
))
|
||||
if cursor.fetchone():
|
||||
result['skipped'] += 1
|
||||
result['skipped_buyer_ids'].append(buyer_id)
|
||||
continue
|
||||
|
||||
self._execute_sql(cursor, """
|
||||
INSERT INTO xy_personal_blacklist
|
||||
(user_id, cookie_id, buyer_id, buyer_nick, item_id, reason, is_enabled, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
""", (
|
||||
user_id,
|
||||
normalized_cookie_id,
|
||||
buyer_id,
|
||||
normalized_buyer_nick,
|
||||
normalized_item_id,
|
||||
normalized_reason,
|
||||
1 if is_enabled else 0,
|
||||
))
|
||||
record_id = cursor.lastrowid
|
||||
self._execute_sql(cursor, """
|
||||
SELECT * FROM xy_personal_blacklist WHERE id = ?
|
||||
""", (record_id,))
|
||||
columns = [desc[0] for desc in cursor.description]
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
result['records'].append(self._personal_blacklist_row_to_dict(row, columns))
|
||||
result['created'] += 1
|
||||
|
||||
self.conn.commit()
|
||||
return result
|
||||
except Exception as e:
|
||||
self.conn.rollback()
|
||||
logger.error(f"创建个人黑名单失败: {e}")
|
||||
return result
|
||||
|
||||
def list_personal_blacklist(
|
||||
self,
|
||||
user_id: int,
|
||||
buyer_id: str = None,
|
||||
buyer_nick: str = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> Dict[str, Any]:
|
||||
"""分页查询个人黑名单。"""
|
||||
safe_page = max(int(page or 1), 1)
|
||||
safe_page_size = min(max(int(page_size or 20), 1), 200)
|
||||
offset = (safe_page - 1) * safe_page_size
|
||||
where_clauses = ["user_id = ?"]
|
||||
params: List[Any] = [user_id]
|
||||
|
||||
normalized_buyer_id = self._normalize_blacklist_scope_value(buyer_id)
|
||||
if normalized_buyer_id:
|
||||
where_clauses.append("buyer_id LIKE ?")
|
||||
params.append(f"%{normalized_buyer_id}%")
|
||||
|
||||
normalized_buyer_nick = self._normalize_blacklist_scope_value(buyer_nick)
|
||||
if normalized_buyer_nick:
|
||||
where_clauses.append("buyer_nick LIKE ?")
|
||||
params.append(f"%{normalized_buyer_nick}%")
|
||||
|
||||
where_sql = " AND ".join(where_clauses)
|
||||
|
||||
with self.lock:
|
||||
cursor = self.conn.cursor()
|
||||
try:
|
||||
self._execute_sql(cursor, f"SELECT COUNT(*) FROM xy_personal_blacklist WHERE {where_sql}", tuple(params))
|
||||
total = int(cursor.fetchone()[0] or 0)
|
||||
|
||||
self._execute_sql(cursor, f"""
|
||||
SELECT * FROM xy_personal_blacklist
|
||||
WHERE {where_sql}
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
""", tuple(params + [safe_page_size, offset]))
|
||||
rows = cursor.fetchall()
|
||||
columns = [desc[0] for desc in cursor.description]
|
||||
data = [self._personal_blacklist_row_to_dict(row, columns) for row in rows]
|
||||
return {
|
||||
'data': data,
|
||||
'total': total,
|
||||
'page': safe_page,
|
||||
'page_size': safe_page_size,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"查询个人黑名单失败: {e}")
|
||||
return {'data': [], 'total': 0, 'page': safe_page, 'page_size': safe_page_size}
|
||||
|
||||
def delete_personal_blacklist(self, record_id: int, user_id: int) -> bool:
|
||||
"""删除单条个人黑名单。"""
|
||||
with self.lock:
|
||||
cursor = self.conn.cursor()
|
||||
try:
|
||||
self._execute_sql(cursor, "DELETE FROM xy_personal_blacklist WHERE id = ? AND user_id = ?", (record_id, user_id))
|
||||
deleted = cursor.rowcount > 0
|
||||
self.conn.commit()
|
||||
return deleted
|
||||
except Exception as e:
|
||||
self.conn.rollback()
|
||||
logger.error(f"删除个人黑名单失败: {e}")
|
||||
return False
|
||||
|
||||
def batch_delete_personal_blacklist(self, ids: List[int], user_id: int) -> int:
|
||||
"""批量删除个人黑名单,返回删除数量。"""
|
||||
safe_ids = []
|
||||
for raw_id in ids or []:
|
||||
try:
|
||||
record_id = int(raw_id)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if record_id > 0 and record_id not in safe_ids:
|
||||
safe_ids.append(record_id)
|
||||
|
||||
if not safe_ids:
|
||||
return 0
|
||||
|
||||
placeholders = ','.join(['?'] * len(safe_ids))
|
||||
with self.lock:
|
||||
cursor = self.conn.cursor()
|
||||
try:
|
||||
self._execute_sql(
|
||||
cursor,
|
||||
f"DELETE FROM xy_personal_blacklist WHERE user_id = ? AND id IN ({placeholders})",
|
||||
tuple([user_id] + safe_ids),
|
||||
)
|
||||
deleted = cursor.rowcount
|
||||
self.conn.commit()
|
||||
return deleted
|
||||
except Exception as e:
|
||||
self.conn.rollback()
|
||||
logger.error(f"批量删除个人黑名单失败: {e}")
|
||||
return 0
|
||||
|
||||
def toggle_personal_blacklist(self, record_id: int, user_id: int, is_enabled: bool) -> bool:
|
||||
"""启用或禁用个人黑名单。"""
|
||||
with self.lock:
|
||||
cursor = self.conn.cursor()
|
||||
try:
|
||||
self._execute_sql(cursor, """
|
||||
UPDATE xy_personal_blacklist
|
||||
SET is_enabled = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND user_id = ?
|
||||
""", (1 if is_enabled else 0, record_id, user_id))
|
||||
updated = cursor.rowcount > 0
|
||||
self.conn.commit()
|
||||
return updated
|
||||
except Exception as e:
|
||||
self.conn.rollback()
|
||||
logger.error(f"更新个人黑名单状态失败: {e}")
|
||||
return False
|
||||
|
||||
def is_buyer_blacklisted(
|
||||
self,
|
||||
user_id: int,
|
||||
buyer_id: str,
|
||||
cookie_id: str = None,
|
||||
item_id: str = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""按商品级 > 账号级 > 用户级匹配个人黑名单。"""
|
||||
normalized_buyer_id = self._normalize_blacklist_scope_value(buyer_id)
|
||||
if not user_id or not normalized_buyer_id:
|
||||
return None
|
||||
|
||||
normalized_cookie_id = self._normalize_blacklist_scope_value(cookie_id)
|
||||
normalized_item_id = self._normalize_blacklist_scope_value(item_id)
|
||||
|
||||
with self.lock:
|
||||
cursor = self.conn.cursor()
|
||||
try:
|
||||
self._execute_sql(cursor, """
|
||||
SELECT * FROM xy_personal_blacklist
|
||||
WHERE user_id = ?
|
||||
AND buyer_id = ?
|
||||
AND is_enabled = 1
|
||||
AND (
|
||||
(COALESCE(cookie_id, '') = '' AND COALESCE(item_id, '') = '')
|
||||
OR (COALESCE(cookie_id, '') = ? AND COALESCE(item_id, '') = '')
|
||||
OR (COALESCE(item_id, '') = ? AND (COALESCE(cookie_id, '') = '' OR COALESCE(cookie_id, '') = ?))
|
||||
)
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN COALESCE(item_id, '') != '' THEN 3
|
||||
WHEN COALESCE(cookie_id, '') != '' THEN 2
|
||||
ELSE 1
|
||||
END DESC,
|
||||
updated_at DESC,
|
||||
id DESC
|
||||
LIMIT 1
|
||||
""", (
|
||||
user_id,
|
||||
normalized_buyer_id,
|
||||
normalized_cookie_id or '',
|
||||
normalized_item_id or '',
|
||||
normalized_cookie_id or '',
|
||||
))
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
columns = [desc[0] for desc in cursor.description]
|
||||
return self._personal_blacklist_row_to_dict(row, columns)
|
||||
except Exception as e:
|
||||
logger.error(f"匹配个人黑名单失败: {e}")
|
||||
return None
|
||||
|
||||
def list_platform_blacklist(self, user_id: int, page: int = 1, page_size: int = 20) -> Dict[str, Any]:
|
||||
"""分页查询平台黑名单(当前仅预留展示)。"""
|
||||
safe_page = max(int(page or 1), 1)
|
||||
safe_page_size = min(max(int(page_size or 20), 1), 200)
|
||||
offset = (safe_page - 1) * safe_page_size
|
||||
|
||||
with self.lock:
|
||||
cursor = self.conn.cursor()
|
||||
try:
|
||||
self._execute_sql(cursor, "SELECT COUNT(*) FROM xy_platform_blacklist WHERE user_id = ?", (user_id,))
|
||||
total = int(cursor.fetchone()[0] or 0)
|
||||
self._execute_sql(cursor, """
|
||||
SELECT id, user_id, buyer_id, buyer_nick, created_at, updated_at
|
||||
FROM xy_platform_blacklist
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
""", (user_id, safe_page_size, offset))
|
||||
rows = cursor.fetchall()
|
||||
columns = [desc[0] for desc in cursor.description]
|
||||
return {
|
||||
'data': [dict(zip(columns, row)) for row in rows],
|
||||
'total': total,
|
||||
'page': safe_page,
|
||||
'page_size': safe_page_size,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"查询平台黑名单失败: {e}")
|
||||
return {'data': [], 'total': 0, 'page': safe_page, 'page_size': safe_page_size}
|
||||
|
||||
|
||||
# 全局单例
|
||||
db_manager = DBManager()
|
||||
|
||||
+275
-1
@@ -1,6 +1,6 @@
|
||||
from fastapi import FastAPI, HTTPException, Depends, status, UploadFile, File, Form, Request, BackgroundTasks
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse, StreamingResponse
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse, StreamingResponse, Response
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Tuple, Optional, Dict, Any, Callable, Awaitable
|
||||
@@ -29,6 +29,7 @@ from db_manager import db_manager
|
||||
from config import RISK_CONTROL
|
||||
from file_log_collector import setup_file_logging, get_file_log_collector
|
||||
from ai_reply_engine import ai_reply_engine
|
||||
from blacklist_service import blacklist_service
|
||||
from utils.qr_login import qr_login_manager
|
||||
from utils.qr_login_lite import qrcode_login_lite
|
||||
from utils.xianyu_utils import trans_cookies
|
||||
@@ -1021,6 +1022,37 @@ def log_with_user(level: str, message: str, user_info: Dict[str, Any] = None):
|
||||
logger.info(full_message)
|
||||
|
||||
|
||||
def _get_blacklist_block_by_cookie(cookie_id: str, buyer_id: str, item_id: str = None) -> Optional[Dict[str, Any]]:
|
||||
"""按 cookie 归属检查买家是否命中个人黑名单。"""
|
||||
try:
|
||||
normalized_cookie_id = str(cookie_id or '').strip()
|
||||
normalized_buyer_id = str(buyer_id or '').strip()
|
||||
if not normalized_cookie_id or not normalized_buyer_id:
|
||||
return None
|
||||
cookie_details = db_manager.get_cookie_details(normalized_cookie_id)
|
||||
user_id = cookie_details.get('user_id') if cookie_details else None
|
||||
if not user_id:
|
||||
return None
|
||||
return blacklist_service.is_buyer_blacklisted(
|
||||
user_id=user_id,
|
||||
buyer_id=normalized_buyer_id,
|
||||
cookie_id=normalized_cookie_id,
|
||||
item_id=str(item_id or '').strip() or None,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"检查黑名单失败: cookie_id={cookie_id}, buyer_id={buyer_id}, error={mask_sensitive_text(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def _format_blacklist_block_message(hit: Dict[str, Any]) -> str:
|
||||
if not hit:
|
||||
return '买家命中黑名单,已拦截'
|
||||
scope_label = {'item': '商品级', 'account': '账号级', 'user': '用户级'}.get(hit.get('scope'), hit.get('scope') or '未知级别')
|
||||
reason = str(hit.get('reason') or '').strip()
|
||||
reason_part = f",原因:{reason}" if reason else ''
|
||||
return f"买家 {hit.get('buyer_id') or ''} 命中{scope_label}黑名单{reason_part},已拦截"
|
||||
|
||||
|
||||
def match_reply(cookie_id: str, message: str) -> Optional[str]:
|
||||
"""根据 cookie_id 及消息内容匹配回复
|
||||
只有启用的账号才会匹配关键字回复
|
||||
@@ -1066,6 +1098,23 @@ class ResponseModel(BaseModel):
|
||||
data: ResponseData
|
||||
|
||||
|
||||
class PersonalBlacklistCreateRequest(BaseModel):
|
||||
buyer_ids: Any
|
||||
cookie_id: Optional[str] = None
|
||||
item_id: Optional[str] = None
|
||||
buyer_nick: Optional[str] = ''
|
||||
reason: Optional[str] = ''
|
||||
is_enabled: bool = True
|
||||
|
||||
|
||||
class PersonalBlacklistBatchDeleteRequest(BaseModel):
|
||||
ids: List[int]
|
||||
|
||||
|
||||
class PersonalBlacklistToggleRequest(BaseModel):
|
||||
is_enabled: bool
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="Xianyu Management API",
|
||||
version="1.0.0",
|
||||
@@ -2321,6 +2370,12 @@ async def send_message_api(request: SendMessageRequest):
|
||||
message=f"参数 {param_name} 不能为空"
|
||||
)
|
||||
|
||||
blacklist_hit = _get_blacklist_block_by_cookie(cleaned_cookie_id, cleaned_to_user_id)
|
||||
if blacklist_hit:
|
||||
block_message = _format_blacklist_block_message(blacklist_hit)
|
||||
logger.warning(f"API发送消息被黑名单拦截: cookie_id={cleaned_cookie_id}, buyer_id={cleaned_to_user_id}, scope={blacklist_hit.get('scope')}")
|
||||
return SendMessageResponse(success=False, message=block_message)
|
||||
|
||||
# 直接获取XianyuLive实例,跳过cookie_manager检查
|
||||
from XianyuAutoAsync import XianyuLive, ConnectionState
|
||||
live_instance = XianyuLive.get_instance(cleaned_cookie_id)
|
||||
@@ -2390,6 +2445,23 @@ async def send_message_api(request: SendMessageRequest):
|
||||
|
||||
@app.post("/xianyu/reply", response_model=ResponseModel)
|
||||
async def xianyu_reply(req: RequestModel):
|
||||
blacklist_hit = _get_blacklist_block_by_cookie(req.cookie_id, req.send_user_id, req.item_id)
|
||||
if blacklist_hit:
|
||||
logger.warning(
|
||||
f"/xianyu/reply 被黑名单拦截: cookie_id={req.cookie_id}, buyer_id={req.send_user_id}, "
|
||||
f"item_id={req.item_id}, scope={blacklist_hit.get('scope')}"
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=403,
|
||||
content={
|
||||
'success': False,
|
||||
'blocked': True,
|
||||
'reason': 'buyer_blacklisted',
|
||||
'message': _format_blacklist_block_message(blacklist_hit),
|
||||
'blacklist': blacklist_hit,
|
||||
},
|
||||
)
|
||||
|
||||
msg_template = match_reply(req.cookie_id, req.send_message)
|
||||
is_default_reply = False
|
||||
|
||||
@@ -2432,6 +2504,168 @@ async def xianyu_reply(req: RequestModel):
|
||||
|
||||
return {"code": 200, "data": {"send_msg": send_msg}}
|
||||
|
||||
|
||||
# ------------------------- 黑名单接口 -------------------------
|
||||
|
||||
|
||||
@app.get('/api/blacklist/personal')
|
||||
def get_personal_blacklist(
|
||||
buyer_id: str = None,
|
||||
buyer_nick: str = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
result = blacklist_service.list_personal(
|
||||
user_id=current_user['user_id'],
|
||||
buyer_id=buyer_id,
|
||||
buyer_nick=buyer_nick,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
return {'success': True, **result}
|
||||
except Exception as e:
|
||||
log_with_user('error', f"查询个人黑名单失败: {mask_sensitive_text(e)}", current_user)
|
||||
raise HTTPException(status_code=500, detail='查询个人黑名单失败')
|
||||
|
||||
|
||||
@app.post('/api/blacklist/personal')
|
||||
def create_personal_blacklist(
|
||||
request: PersonalBlacklistCreateRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
cookie_id = str(request.cookie_id or '').strip() or None
|
||||
if cookie_id:
|
||||
cookie_id = _ensure_cookie_access(cookie_id, current_user)
|
||||
|
||||
result = blacklist_service.create_personal(
|
||||
user_id=current_user['user_id'],
|
||||
buyer_ids=request.buyer_ids,
|
||||
cookie_id=cookie_id,
|
||||
item_id=str(request.item_id or '').strip() or None,
|
||||
reason=str(request.reason or '').strip(),
|
||||
is_enabled=bool(request.is_enabled),
|
||||
buyer_nick=str(request.buyer_nick or '').strip(),
|
||||
)
|
||||
created = int(result.get('created') or 0)
|
||||
skipped = int(result.get('skipped') or 0)
|
||||
message = f"成功添加 {created} 条黑名单"
|
||||
if skipped:
|
||||
message += f",跳过 {skipped} 条"
|
||||
log_with_user('info', f"新增个人黑名单: created={created}, skipped={skipped}", current_user)
|
||||
return {'success': True, 'message': message, 'data': {'count': created, 'skipped': skipped, 'records': result.get('records') or []}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
log_with_user('error', f"新增个人黑名单失败: {mask_sensitive_text(e)}", current_user)
|
||||
raise HTTPException(status_code=500, detail='新增个人黑名单失败')
|
||||
|
||||
|
||||
@app.post('/api/blacklist/personal/batch-delete')
|
||||
def batch_delete_personal_blacklist(
|
||||
request: PersonalBlacklistBatchDeleteRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
deleted = blacklist_service.batch_delete_personal(request.ids, current_user['user_id'])
|
||||
return {'success': True, 'message': f'成功删除 {deleted} 条黑名单', 'data': {'deleted': deleted}}
|
||||
except Exception as e:
|
||||
log_with_user('error', f"批量删除个人黑名单失败: {mask_sensitive_text(e)}", current_user)
|
||||
raise HTTPException(status_code=500, detail='批量删除个人黑名单失败')
|
||||
|
||||
|
||||
@app.patch('/api/blacklist/personal/{record_id}/toggle')
|
||||
def toggle_personal_blacklist(
|
||||
record_id: int,
|
||||
request: PersonalBlacklistToggleRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
success = blacklist_service.toggle_personal(record_id, current_user['user_id'], request.is_enabled)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail='黑名单记录不存在')
|
||||
return {'success': True, 'message': '状态已更新'}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
log_with_user('error', f"更新个人黑名单状态失败: {mask_sensitive_text(e)}", current_user)
|
||||
raise HTTPException(status_code=500, detail='更新个人黑名单状态失败')
|
||||
|
||||
|
||||
@app.delete('/api/blacklist/personal/{record_id}')
|
||||
def delete_personal_blacklist(
|
||||
record_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
success = blacklist_service.delete_personal(record_id, current_user['user_id'])
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail='黑名单记录不存在')
|
||||
return {'success': True, 'message': '删除成功'}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
log_with_user('error', f"删除个人黑名单失败: {mask_sensitive_text(e)}", current_user)
|
||||
raise HTTPException(status_code=500, detail='删除个人黑名单失败')
|
||||
|
||||
|
||||
@app.get('/api/blacklist/personal/export')
|
||||
def export_personal_blacklist(current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
try:
|
||||
content = blacklist_service.export_personal_xlsx(current_user['user_id'])
|
||||
filename = f"personal_blacklist_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
|
||||
return Response(
|
||||
content=content,
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers=headers,
|
||||
)
|
||||
except Exception as e:
|
||||
log_with_user('error', f"导出个人黑名单失败: {mask_sensitive_text(e)}", current_user)
|
||||
raise HTTPException(status_code=500, detail='导出个人黑名单失败')
|
||||
|
||||
|
||||
@app.post('/api/blacklist/personal/import')
|
||||
async def import_personal_blacklist(
|
||||
file: UploadFile = File(...),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
filename = file.filename or ''
|
||||
if not filename.lower().endswith('.xlsx'):
|
||||
raise HTTPException(status_code=400, detail='仅支持 .xlsx 文件')
|
||||
content = await file.read()
|
||||
result = blacklist_service.import_personal_xlsx(current_user['user_id'], content)
|
||||
return {
|
||||
'success': True,
|
||||
'message': f"导入完成:新增 {result.get('created', 0)} 条,跳过 {result.get('skipped', 0)} 条",
|
||||
'data': result,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
log_with_user('error', f"导入个人黑名单失败: {mask_sensitive_text(e)}", current_user)
|
||||
raise HTTPException(status_code=500, detail='导入个人黑名单失败')
|
||||
|
||||
|
||||
@app.get('/api/blacklist/platform')
|
||||
def get_platform_blacklist(
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
result = blacklist_service.list_platform(current_user['user_id'], page=page, page_size=page_size)
|
||||
return {'success': True, **result}
|
||||
except Exception as e:
|
||||
log_with_user('error', f"查询平台黑名单失败: {mask_sensitive_text(e)}", current_user)
|
||||
raise HTTPException(status_code=500, detail='查询平台黑名单失败')
|
||||
|
||||
|
||||
# ------------------------- 账号 / 关键字管理接口 -------------------------
|
||||
|
||||
|
||||
@@ -12987,6 +13221,20 @@ async def chat_send_message(
|
||||
if not live_instance.ws:
|
||||
raise HTTPException(status_code=400, detail="WebSocket连接未就绪")
|
||||
|
||||
item_id_for_blacklist = None
|
||||
try:
|
||||
recent_order = db_manager.get_recent_order_by_sid(req.chat_id, cookie_id, minutes=60)
|
||||
if recent_order and str(recent_order.get('buyer_id') or '') == str(req.to_user_id or ''):
|
||||
item_id_for_blacklist = recent_order.get('item_id')
|
||||
except Exception:
|
||||
item_id_for_blacklist = None
|
||||
|
||||
blacklist_hit = _get_blacklist_block_by_cookie(cookie_id, req.to_user_id, item_id_for_blacklist)
|
||||
if blacklist_hit:
|
||||
block_message = _format_blacklist_block_message(blacklist_hit)
|
||||
logger.warning(f"客服发送消息被黑名单拦截: cookie_id={cookie_id}, buyer_id={req.to_user_id}, scope={blacklist_hit.get('scope')}")
|
||||
return {'success': False, 'blocked': True, 'message': block_message, 'blacklist': blacklist_hit}
|
||||
|
||||
await _run_live_instance_on_manager_loop(
|
||||
cookie_id,
|
||||
lambda: live_instance.send_msg(
|
||||
@@ -13251,6 +13499,32 @@ async def manual_deliver_order(order_id: str, current_user: Dict[str, Any] = Dep
|
||||
if not buyer_id:
|
||||
return {"success": False, "delivered": False, "message": "订单缺少买家信息,无法发送消息"}
|
||||
|
||||
blacklist_hit = blacklist_service.is_buyer_blacklisted(
|
||||
user_id=user_id,
|
||||
buyer_id=buyer_id,
|
||||
cookie_id=cookie_id,
|
||||
item_id=item_id,
|
||||
)
|
||||
if blacklist_hit:
|
||||
block_message = _format_blacklist_block_message(blacklist_hit)
|
||||
db_manager.create_delivery_log(
|
||||
user_id=user_id,
|
||||
cookie_id=cookie_id,
|
||||
order_id=order_id,
|
||||
item_id=item_id,
|
||||
buyer_id=buyer_id,
|
||||
buyer_nick=order.get('buyer_nick'),
|
||||
rule_id=None,
|
||||
rule_keyword=None,
|
||||
card_type=None,
|
||||
match_mode='blacklist',
|
||||
channel='manual',
|
||||
status='skipped',
|
||||
reason=block_message,
|
||||
)
|
||||
log_with_user('warning', f"手动发货被黑名单拦截: order_id={order_id}, buyer_id={buyer_id}, scope={blacklist_hit.get('scope')}", current_user)
|
||||
return {"success": False, "delivered": False, "blocked": True, "message": block_message, "blacklist": blacklist_hit}
|
||||
|
||||
# 获取商品标题
|
||||
item_info = db_manager.get_item_info(cookie_id, item_id)
|
||||
item_title = item_info.get('item_title', '') if item_info else ''
|
||||
|
||||
@@ -143,6 +143,12 @@
|
||||
<span>在线客服</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-item" data-menu-id="blacklist">
|
||||
<a href="javascript:void(0)" class="nav-link" onclick="showSection('blacklist')">
|
||||
<i class="bi bi-person-x"></i>
|
||||
<span>黑名单管理</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-item" data-menu-id="system-settings">
|
||||
<a href="javascript:void(0)" class="nav-link" onclick="showSection('system-settings')">
|
||||
<i class="bi bi-gear"></i>
|
||||
@@ -3138,6 +3144,154 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 黑名单管理内容 -->
|
||||
<div id="blacklist-section" class="content-section">
|
||||
<div class="content-header">
|
||||
<div class="d-flex justify-content-between align-items-center w-100 flex-wrap gap-2">
|
||||
<div>
|
||||
<h2 class="mb-0">
|
||||
<i class="bi bi-person-x me-2"></i>
|
||||
黑名单管理
|
||||
</h2>
|
||||
<p class="text-muted mb-0">按用户、账号或商品范围拦截指定买家的自动回复、客服发送和自动发货</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button type="button" class="btn btn-outline-primary" onclick="document.getElementById('blacklistImportFile').click()">
|
||||
<i class="bi bi-upload me-1"></i>导入
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-success" onclick="exportPersonalBlacklist()">
|
||||
<i class="bi bi-download me-1"></i>导出
|
||||
</button>
|
||||
<input type="file" id="blacklistImportFile" accept=".xlsx" class="d-none" onchange="importPersonalBlacklistFile()">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-body">
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-plus-circle me-2"></i>新增个人黑名单
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="personalBlacklistForm" onsubmit="event.preventDefault(); createPersonalBlacklist(); return false;">
|
||||
<div class="mb-3">
|
||||
<label for="blacklistBuyerIds" class="form-label">买家ID <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="blacklistBuyerIds" rows="4" placeholder="每行一个买家ID,也支持逗号分隔" required></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="blacklistCookieId" class="form-label">作用账号</label>
|
||||
<select class="form-select" id="blacklistCookieId">
|
||||
<option value="">全部账号</option>
|
||||
</select>
|
||||
<div class="form-text">选择账号后仅拦截该账号;留空为用户级。</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="blacklistItemId" class="form-label">商品ID</label>
|
||||
<input type="text" class="form-control" id="blacklistItemId" placeholder="可选,填写后仅拦截该商品">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="blacklistBuyerNick" class="form-label">买家昵称</label>
|
||||
<input type="text" class="form-control" id="blacklistBuyerNick" placeholder="可选,便于识别">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="blacklistReason" class="form-label">拉黑原因</label>
|
||||
<textarea class="form-control" id="blacklistReason" rows="2" placeholder="可选,会显示在拦截提示和发货日志中"></textarea>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="blacklistEnabled" checked>
|
||||
<label class="form-check-label" for="blacklistEnabled">立即启用</label>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary flex-fill">
|
||||
<i class="bi bi-check-circle me-1"></i>保存
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="resetPersonalBlacklistForm()">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div>
|
||||
<i class="bi bi-list-check me-2"></i>个人黑名单
|
||||
<span class="text-muted small ms-2" id="blacklistTotalText">共 0 条</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" id="blacklistBatchDeleteBtn" onclick="batchDeletePersonalBlacklist()" disabled>
|
||||
<i class="bi bi-trash me-1"></i>批量删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-2 align-items-end mb-3">
|
||||
<div class="col-md-4">
|
||||
<label for="blacklistFilterBuyerId" class="form-label small mb-1">买家ID</label>
|
||||
<input type="text" class="form-control form-control-sm" id="blacklistFilterBuyerId" placeholder="搜索买家ID" onkeydown="handleBlacklistFilterKeydown(event)">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="blacklistFilterBuyerNick" class="form-label small mb-1">买家昵称</label>
|
||||
<input type="text" class="form-control form-control-sm" id="blacklistFilterBuyerNick" placeholder="搜索昵称" onkeydown="handleBlacklistFilterKeydown(event)">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label for="blacklistPageSize" class="form-label small mb-1">每页</label>
|
||||
<select class="form-select form-select-sm" id="blacklistPageSize" onchange="loadPersonalBlacklist(1)">
|
||||
<option value="10">10</option>
|
||||
<option value="20" selected>20</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex gap-2">
|
||||
<button type="button" class="btn btn-primary btn-sm flex-fill" onclick="loadPersonalBlacklist(1)">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="resetBlacklistFilters()">
|
||||
<i class="bi bi-x-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 36px;"><input class="form-check-input" type="checkbox" id="blacklistSelectAll" onchange="toggleBlacklistSelectAll(this.checked)"></th>
|
||||
<th>范围</th>
|
||||
<th>买家</th>
|
||||
<th>目标</th>
|
||||
<th>原因</th>
|
||||
<th>状态</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="blacklistTableBody">
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-4 text-muted">
|
||||
<i class="bi bi-person-x fs-1 d-block mb-3"></i>
|
||||
暂无黑名单记录
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mt-3">
|
||||
<div class="small text-muted" id="blacklistPageText">第 1 页</div>
|
||||
<div class="btn-group" id="blacklistPagination" role="group" aria-label="黑名单分页"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统设置内容 -->
|
||||
<div id="system-settings-section" class="content-section">
|
||||
<div class="content-header">
|
||||
|
||||
+400
-1
@@ -62,6 +62,12 @@ let orderHistorySyncPollingTimer = null;
|
||||
let activeOrderHistorySyncJobId = '';
|
||||
let orderHistorySyncNotifiedJobId = '';
|
||||
let orderHistorySyncAccounts = [];
|
||||
let blacklistState = {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
accountsLoaded: false
|
||||
};
|
||||
let loadingRequestCount = 0;
|
||||
let loadingShowTimer = null;
|
||||
const LOADING_SHOW_DELAY = 120;
|
||||
@@ -172,6 +178,9 @@ function showSection(sectionName) {
|
||||
case 'online-im': // 【在线客服菜单】
|
||||
loadOnlineIm();
|
||||
break;
|
||||
case 'blacklist': // 【黑名单管理菜单】
|
||||
loadBlacklistPage();
|
||||
break;
|
||||
case 'data-management': // 【数据管理菜单】
|
||||
loadDataManagement();
|
||||
break;
|
||||
@@ -3376,9 +3385,398 @@ async function fetchJSON(url, opts = {}) {
|
||||
}
|
||||
|
||||
// ================================
|
||||
// 账号保活诊断
|
||||
// 【黑名单管理菜单】相关功能
|
||||
// ================================
|
||||
|
||||
async function loadBlacklistPage() {
|
||||
await loadBlacklistAccountOptions();
|
||||
await loadPersonalBlacklist(blacklistState.page || 1);
|
||||
}
|
||||
|
||||
async function loadBlacklistAccountOptions(force = false) {
|
||||
const accountSelect = document.getElementById('blacklistCookieId');
|
||||
if (!accountSelect) return;
|
||||
if (blacklistState.accountsLoaded && !force) return;
|
||||
|
||||
try {
|
||||
const currentValue = accountSelect.value;
|
||||
const accounts = await fetchJSON(`${apiBase}/cookies/details`);
|
||||
const safeAccounts = Array.isArray(accounts) ? accounts : [];
|
||||
accountSelect.innerHTML = '<option value="">全部账号</option>' + safeAccounts.map(account => {
|
||||
const accountId = String(account.id || '').trim();
|
||||
const remark = String(account.remark || '').trim();
|
||||
const label = remark ? `${accountId}(${remark})` : accountId;
|
||||
return `<option value="${escapeHtml(accountId)}">${escapeHtml(label)}</option>`;
|
||||
}).join('');
|
||||
if (currentValue && safeAccounts.some(account => String(account.id || '') === currentValue)) {
|
||||
accountSelect.value = currentValue;
|
||||
}
|
||||
blacklistState.accountsLoaded = true;
|
||||
} catch (error) {
|
||||
console.error('加载黑名单账号选项失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleBlacklistFilterKeydown(event) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
loadPersonalBlacklist(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPersonalBlacklist(page = 1) {
|
||||
const tableBody = document.getElementById('blacklistTableBody');
|
||||
if (!tableBody) return;
|
||||
|
||||
const pageSizeSelect = document.getElementById('blacklistPageSize');
|
||||
const pageSize = Math.max(1, parseInt(pageSizeSelect?.value || '20', 10) || 20);
|
||||
const safePage = Math.max(1, parseInt(page, 10) || 1);
|
||||
const params = new URLSearchParams({
|
||||
page: String(safePage),
|
||||
page_size: String(pageSize)
|
||||
});
|
||||
|
||||
const buyerId = document.getElementById('blacklistFilterBuyerId')?.value?.trim();
|
||||
const buyerNick = document.getElementById('blacklistFilterBuyerNick')?.value?.trim();
|
||||
if (buyerId) params.set('buyer_id', buyerId);
|
||||
if (buyerNick) params.set('buyer_nick', buyerNick);
|
||||
|
||||
try {
|
||||
const result = await fetchJSON(`${apiBase}/api/blacklist/personal?${params.toString()}`);
|
||||
const records = Array.isArray(result?.data) ? result.data : [];
|
||||
blacklistState.page = Number(result?.page || safePage);
|
||||
blacklistState.pageSize = Number(result?.page_size || pageSize);
|
||||
blacklistState.total = Number(result?.total || 0);
|
||||
renderPersonalBlacklist(records);
|
||||
renderBlacklistPagination();
|
||||
} catch (error) {
|
||||
console.error('加载个人黑名单失败:', error);
|
||||
tableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-4 text-danger">
|
||||
<i class="bi bi-exclamation-triangle fs-1 d-block mb-3"></i>
|
||||
加载黑名单失败
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function getBlacklistScopeBadge(scope) {
|
||||
const normalizedScope = String(scope || 'user');
|
||||
const config = {
|
||||
item: { text: '商品级', cls: 'bg-warning text-dark' },
|
||||
account: { text: '账号级', cls: 'bg-info text-dark' },
|
||||
user: { text: '用户级', cls: 'bg-secondary' }
|
||||
}[normalizedScope] || { text: normalizedScope || '未知', cls: 'bg-secondary' };
|
||||
return `<span class="badge ${config.cls}">${escapeHtml(config.text)}</span>`;
|
||||
}
|
||||
|
||||
function getBlacklistTargetHtml(record) {
|
||||
const cookieId = String(record?.cookie_id || '').trim();
|
||||
const itemId = String(record?.item_id || '').trim();
|
||||
const parts = [];
|
||||
parts.push(cookieId ? `账号 ${cookieId}` : '全部账号');
|
||||
if (itemId) parts.push(`商品 ${itemId}`);
|
||||
return parts.map(part => `<div class="small text-muted text-nowrap" title="${escapeHtml(part)}">${escapeHtml(part)}</div>`).join('');
|
||||
}
|
||||
|
||||
function renderPersonalBlacklist(records) {
|
||||
const tableBody = document.getElementById('blacklistTableBody');
|
||||
const totalText = document.getElementById('blacklistTotalText');
|
||||
const selectAll = document.getElementById('blacklistSelectAll');
|
||||
if (!tableBody) return;
|
||||
|
||||
if (totalText) {
|
||||
totalText.textContent = `共 ${blacklistState.total || 0} 条`;
|
||||
}
|
||||
if (selectAll) {
|
||||
selectAll.checked = false;
|
||||
selectAll.indeterminate = false;
|
||||
}
|
||||
|
||||
if (!Array.isArray(records) || records.length === 0) {
|
||||
tableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-4 text-muted">
|
||||
<i class="bi bi-person-x fs-1 d-block mb-3"></i>
|
||||
暂无黑名单记录
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
updateBlacklistBatchDeleteState();
|
||||
return;
|
||||
}
|
||||
|
||||
tableBody.innerHTML = records.map(record => {
|
||||
const recordId = Number(record.id || 0);
|
||||
const buyerId = String(record.buyer_id || '').trim();
|
||||
const buyerNick = String(record.buyer_nick || '').trim();
|
||||
const reason = String(record.reason || '').trim();
|
||||
const enabled = Boolean(record.is_enabled);
|
||||
const createdAt = formatDateTime(record.created_at || record.updated_at || '');
|
||||
return `
|
||||
<tr>
|
||||
<td>
|
||||
<input class="form-check-input blacklist-row-check" type="checkbox" data-id="${recordId}" onchange="updateBlacklistBatchDeleteState()">
|
||||
</td>
|
||||
<td>${getBlacklistScopeBadge(record.scope)}</td>
|
||||
<td>
|
||||
<div class="fw-semibold" title="${escapeHtml(buyerId)}">${escapeHtml(buyerId)}</div>
|
||||
${buyerNick ? `<div class="small text-muted" title="${escapeHtml(buyerNick)}">${escapeHtml(buyerNick)}</div>` : ''}
|
||||
</td>
|
||||
<td>${getBlacklistTargetHtml(record)}</td>
|
||||
<td style="max-width: 220px;">
|
||||
<span class="d-inline-block text-truncate" style="max-width: 100%;" title="${escapeHtml(reason)}">${escapeHtml(reason || '-')}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="form-check form-switch m-0" title="${enabled ? '点击禁用' : '点击启用'}">
|
||||
<input class="form-check-input" type="checkbox" ${enabled ? 'checked' : ''} onchange="togglePersonalBlacklist(${recordId}, this.checked)">
|
||||
</div>
|
||||
</td>
|
||||
<td><small class="text-muted text-nowrap">${escapeHtml(createdAt)}</small></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deletePersonalBlacklist(${recordId})" title="删除">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
updateBlacklistBatchDeleteState();
|
||||
}
|
||||
|
||||
function renderBlacklistPagination() {
|
||||
const pagination = document.getElementById('blacklistPagination');
|
||||
const pageText = document.getElementById('blacklistPageText');
|
||||
if (!pagination) return;
|
||||
|
||||
const pageSize = Math.max(1, Number(blacklistState.pageSize || 20));
|
||||
const total = Math.max(0, Number(blacklistState.total || 0));
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const currentPage = Math.min(Math.max(1, Number(blacklistState.page || 1)), totalPages);
|
||||
|
||||
if (currentPage !== blacklistState.page && total > 0) {
|
||||
loadPersonalBlacklist(currentPage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pageText) {
|
||||
pageText.textContent = `第 ${currentPage} / ${totalPages} 页`;
|
||||
}
|
||||
|
||||
const startPage = Math.max(1, currentPage - 2);
|
||||
const endPage = Math.min(totalPages, startPage + 4);
|
||||
const buttons = [];
|
||||
const addButton = (label, targetPage, disabled = false, active = false, title = '') => {
|
||||
buttons.push(`
|
||||
<button type="button" class="btn btn-sm ${active ? 'btn-primary' : 'btn-outline-secondary'}" ${disabled ? 'disabled' : ''} onclick="loadPersonalBlacklist(${targetPage})" title="${escapeHtml(title || label)}">
|
||||
${label}
|
||||
</button>
|
||||
`);
|
||||
};
|
||||
|
||||
addButton('<i class="bi bi-chevron-left"></i>', currentPage - 1, currentPage <= 1, false, '上一页');
|
||||
for (let page = startPage; page <= endPage; page += 1) {
|
||||
addButton(String(page), page, false, page === currentPage);
|
||||
}
|
||||
addButton('<i class="bi bi-chevron-right"></i>', currentPage + 1, currentPage >= totalPages, false, '下一页');
|
||||
pagination.innerHTML = buttons.join('');
|
||||
}
|
||||
|
||||
async function createPersonalBlacklist() {
|
||||
const buyerIds = document.getElementById('blacklistBuyerIds')?.value?.trim() || '';
|
||||
if (!buyerIds) {
|
||||
showToast('请填写买家ID', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
buyer_ids: buyerIds,
|
||||
cookie_id: document.getElementById('blacklistCookieId')?.value?.trim() || null,
|
||||
item_id: document.getElementById('blacklistItemId')?.value?.trim() || null,
|
||||
buyer_nick: document.getElementById('blacklistBuyerNick')?.value?.trim() || '',
|
||||
reason: document.getElementById('blacklistReason')?.value?.trim() || '',
|
||||
is_enabled: Boolean(document.getElementById('blacklistEnabled')?.checked)
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await fetchJSON(`${apiBase}/api/blacklist/personal`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
showToast(result?.message || '黑名单已保存', 'success');
|
||||
resetPersonalBlacklistForm();
|
||||
await loadPersonalBlacklist(1);
|
||||
} catch (error) {
|
||||
console.error('新增个人黑名单失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function resetPersonalBlacklistForm() {
|
||||
const form = document.getElementById('personalBlacklistForm');
|
||||
if (form) form.reset();
|
||||
const enabled = document.getElementById('blacklistEnabled');
|
||||
if (enabled) enabled.checked = true;
|
||||
}
|
||||
|
||||
async function togglePersonalBlacklist(recordId, isEnabled) {
|
||||
try {
|
||||
await fetchJSON(`${apiBase}/api/blacklist/personal/${recordId}/toggle`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ is_enabled: Boolean(isEnabled) })
|
||||
});
|
||||
showToast(isEnabled ? '黑名单已启用' : '黑名单已禁用', 'success');
|
||||
} catch (error) {
|
||||
console.error('更新黑名单状态失败:', error);
|
||||
await loadPersonalBlacklist(blacklistState.page || 1);
|
||||
}
|
||||
}
|
||||
|
||||
async function deletePersonalBlacklist(recordId) {
|
||||
if (!confirm('确定删除这条黑名单记录吗?')) return;
|
||||
try {
|
||||
const result = await fetchJSON(`${apiBase}/api/blacklist/personal/${recordId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
showToast(result?.message || '黑名单已删除', 'success');
|
||||
await loadPersonalBlacklist(blacklistState.page || 1);
|
||||
} catch (error) {
|
||||
console.error('删除个人黑名单失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedBlacklistIds() {
|
||||
return Array.from(document.querySelectorAll('.blacklist-row-check:checked'))
|
||||
.map(checkbox => parseInt(checkbox.dataset.id || '0', 10))
|
||||
.filter(id => id > 0);
|
||||
}
|
||||
|
||||
function toggleBlacklistSelectAll(checked) {
|
||||
document.querySelectorAll('.blacklist-row-check').forEach(checkbox => {
|
||||
checkbox.checked = Boolean(checked);
|
||||
});
|
||||
updateBlacklistBatchDeleteState();
|
||||
}
|
||||
|
||||
function updateBlacklistBatchDeleteState() {
|
||||
const selectedIds = getSelectedBlacklistIds();
|
||||
const batchButton = document.getElementById('blacklistBatchDeleteBtn');
|
||||
const selectAll = document.getElementById('blacklistSelectAll');
|
||||
const rowChecks = Array.from(document.querySelectorAll('.blacklist-row-check'));
|
||||
|
||||
if (batchButton) {
|
||||
batchButton.disabled = selectedIds.length === 0;
|
||||
batchButton.innerHTML = selectedIds.length > 0
|
||||
? `<i class="bi bi-trash me-1"></i>批量删除 (${selectedIds.length})`
|
||||
: '<i class="bi bi-trash me-1"></i>批量删除';
|
||||
}
|
||||
|
||||
if (selectAll) {
|
||||
selectAll.checked = rowChecks.length > 0 && selectedIds.length === rowChecks.length;
|
||||
selectAll.indeterminate = selectedIds.length > 0 && selectedIds.length < rowChecks.length;
|
||||
}
|
||||
}
|
||||
|
||||
async function batchDeletePersonalBlacklist() {
|
||||
const selectedIds = getSelectedBlacklistIds();
|
||||
if (selectedIds.length === 0) {
|
||||
showToast('请先选择要删除的黑名单', 'warning');
|
||||
return;
|
||||
}
|
||||
if (!confirm(`确定删除选中的 ${selectedIds.length} 条黑名单记录吗?`)) return;
|
||||
|
||||
try {
|
||||
const result = await fetchJSON(`${apiBase}/api/blacklist/personal/batch-delete`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids: selectedIds })
|
||||
});
|
||||
showToast(result?.message || '批量删除完成', 'success');
|
||||
await loadPersonalBlacklist(blacklistState.page || 1);
|
||||
} catch (error) {
|
||||
console.error('批量删除个人黑名单失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function resetBlacklistFilters() {
|
||||
const buyerId = document.getElementById('blacklistFilterBuyerId');
|
||||
const buyerNick = document.getElementById('blacklistFilterBuyerNick');
|
||||
if (buyerId) buyerId.value = '';
|
||||
if (buyerNick) buyerNick.value = '';
|
||||
loadPersonalBlacklist(1);
|
||||
}
|
||||
|
||||
async function exportPersonalBlacklist() {
|
||||
toggleLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${apiBase}/api/blacklist/personal/export`, {
|
||||
headers: { 'Authorization': `Bearer ${getAuthToken()}` }
|
||||
});
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('auth_token');
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
if (!response.ok) {
|
||||
let message = `导出失败: HTTP ${response.status}`;
|
||||
try {
|
||||
const errorText = await response.text();
|
||||
if (errorText) message = errorText;
|
||||
} catch {}
|
||||
throw new Error(message);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const disposition = response.headers.get('Content-Disposition') || '';
|
||||
const filenameMatch = disposition.match(/filename="?([^";]+)"?/i);
|
||||
const filename = filenameMatch ? filenameMatch[1] : `personal_blacklist_${Date.now()}.xlsx`;
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
showToast('黑名单已导出', 'success');
|
||||
} catch (error) {
|
||||
console.error('导出个人黑名单失败:', error);
|
||||
showToast(error.message || '导出个人黑名单失败', 'danger');
|
||||
} finally {
|
||||
toggleLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function importPersonalBlacklistFile() {
|
||||
const input = document.getElementById('blacklistImportFile');
|
||||
const file = input?.files?.[0];
|
||||
if (!file) return;
|
||||
if (!file.name.toLowerCase().endsWith('.xlsx')) {
|
||||
showToast('仅支持 .xlsx 文件', 'warning');
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
try {
|
||||
const result = await fetchJSON(`${apiBase}/api/blacklist/personal/import`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
showToast(result?.message || '黑名单导入完成', 'success');
|
||||
await loadPersonalBlacklist(1);
|
||||
} catch (error) {
|
||||
console.error('导入个人黑名单失败:', error);
|
||||
} finally {
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getAboutDiagnosticsElements() {
|
||||
return {
|
||||
accountSelect: document.getElementById('aboutDiagnosticsAccount'),
|
||||
@@ -9313,6 +9711,7 @@ const DEFAULT_MENU_ITEMS = [
|
||||
{ id: 'notification-channels', name: '通知渠道', icon: 'bi-bell', required: false },
|
||||
{ id: 'message-notifications', name: '消息通知', icon: 'bi-chat-dots', required: false },
|
||||
{ id: 'online-im', name: '在线客服', icon: 'bi-headset', required: false },
|
||||
{ id: 'blacklist', name: '黑名单管理', icon: 'bi-person-x', required: false },
|
||||
{ id: 'system-settings', name: '系统设置', icon: 'bi-gear', required: true },
|
||||
{ id: 'about', name: '关于', icon: 'bi-info-circle', required: true }
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user