fix: 修复买家昵称污染与停止脚本清理

This commit is contained in:
GuDong
2026-05-18 17:48:08 +08:00
parent e77edd0758
commit 2d075eaa33
3 changed files with 297 additions and 34 deletions
+112 -12
View File
@@ -2333,6 +2333,8 @@ class XianyuLive:
"全部",
"交易消息",
"等待你发货",
"买家",
"工作台通知",
"你人真不错,送你闲鱼小红花",
"卖家人不错?送Ta闲鱼小红花",
"快给ta一个评价吧~",
@@ -4607,7 +4609,8 @@ class XianyuLive:
send_user_id=user_id,
item_id=item_id,
error_message=finalize_result.get('error') or '检测到已发送记录,但补完成发货收尾失败',
chat_id=chat_id
chat_id=chat_id,
order_id=order_id
)
return
@@ -4640,7 +4643,8 @@ class XianyuLive:
send_user_id=user_id,
item_id=item_id,
error_message="发货成功",
chat_id=chat_id
chat_id=chat_id,
order_id=order_id
)
logger.info(f'[{msg_time}] 【{self.cookie_id}】[{msg_id}] ✅ 简化消息自动发货补完成收尾成功')
return
@@ -4741,7 +4745,8 @@ class XianyuLive:
send_user_id=user_id,
item_id=item_id,
error_message=finalize_result.get('error') or '发送成功但提交发货副作用失败',
chat_id=chat_id
chat_id=chat_id,
order_id=order_id
)
return
@@ -4789,7 +4794,8 @@ class XianyuLive:
send_user_id=user_id,
item_id=item_id,
error_message="发货成功",
chat_id=chat_id
chat_id=chat_id,
order_id=order_id
)
logger.info(f'[{msg_time}] 【{self.cookie_id}】[{msg_id}] ✅ 简化消息自动发货完成')
@@ -4809,7 +4815,8 @@ class XianyuLive:
send_user_id=user_id,
item_id=item_id,
error_message="未找到匹配的发货规则或获取发货内容失败",
chat_id=chat_id
chat_id=chat_id,
order_id=order_id
)
except Exception as e:
@@ -5511,9 +5518,9 @@ class XianyuLive:
)
else:
notify_message = f"多数量发货成功,共完成 {finalized_count}/{quantity_to_send} 个卡券"
await self.send_delivery_failure_notification(send_user_name, send_user_id, item_id, notify_message, chat_id)
await self.send_delivery_failure_notification(send_user_name, send_user_id, item_id, notify_message, chat_id, order_id=order_id)
else:
await self.send_delivery_failure_notification(send_user_name, send_user_id, item_id, "发货成功", chat_id)
await self.send_delivery_failure_notification(send_user_name, send_user_id, item_id, "发货成功", chat_id, order_id=order_id)
else:
logger.warning(f'[{msg_time}] 【自动发货】未找到匹配的发货规则或获取发货内容失败')
self._record_delivery_log(
@@ -5525,7 +5532,7 @@ class XianyuLive:
reason=last_delivery_error or "未找到匹配的发货规则或获取发货内容失败",
channel='auto'
)
await self.send_delivery_failure_notification(send_user_name, send_user_id, item_id, last_delivery_error or "未找到匹配的发货规则或获取发货内容失败", chat_id)
await self.send_delivery_failure_notification(send_user_name, send_user_id, item_id, last_delivery_error or "未找到匹配的发货规则或获取发货内容失败", chat_id, order_id=order_id)
except Exception as e:
self._record_delivery_log(
@@ -5539,7 +5546,7 @@ class XianyuLive:
)
logger.error(f"自动发货处理异常: {self._safe_str(e)}")
# 发送自动发货异常通知
await self.send_delivery_failure_notification(send_user_name, send_user_id, item_id, f"自动发货处理异常: {str(e)}", chat_id)
await self.send_delivery_failure_notification(send_user_name, send_user_id, item_id, f"自动发货处理异常: {str(e)}", chat_id, order_id=order_id)
logger.info(f'[{msg_time}] 【{self.cookie_id}】订单锁释放: {lock_key},自动发货处理完成')
@@ -9861,13 +9868,106 @@ class XianyuLive:
return "Token定时刷新失败,将自动重试"
async def send_delivery_failure_notification(self, send_user_name: str, send_user_id: str, item_id: str, error_message: str, chat_id: str = None):
"""发送自动发货失败通知"""
def _resolve_delivery_notification_buyer_name(
self,
buyer_name: Any = None,
*,
buyer_id: str = None,
chat_id: str = None,
order_id: str = None,
log_prefix: str = "",
) -> str:
"""为自动发货通知解析可信买家昵称,避免使用“等待你发货”等系统标题。"""
normalized_buyer_id = self._normalize_buyer_id_value(buyer_id)
normalized_chat_id = str(chat_id or '').strip()
try:
if order_id:
order_info = db_manager.get_order_by_id(str(order_id).strip())
if order_info:
order_cookie_id = str(order_info.get('cookie_id') or '').strip()
if not order_cookie_id or order_cookie_id == str(self.cookie_id).strip():
order_buyer_nick = self._sanitize_buyer_nick(
order_info.get('buyer_nick'),
source='delivery_notification_order',
log_prefix=log_prefix,
)
if order_buyer_nick:
return order_buyer_nick
if not normalized_buyer_id:
normalized_buyer_id = self._normalize_buyer_id_value(order_info.get('buyer_id'))
if not normalized_chat_id:
sid = str(order_info.get('sid') or '').strip()
normalized_chat_id = sid.split('@')[0].strip() if sid else ''
if normalized_chat_id:
chat_messages = db_manager.get_chat_messages(self.cookie_id, normalized_chat_id, limit=80)
for chat_message in reversed(chat_messages or []):
if int(chat_message.get('direction') or 0) != 2:
continue
sender_id = self._normalize_buyer_id_value(chat_message.get('sender_id'))
if sender_id and sender_id == self.myid:
continue
if normalized_buyer_id and sender_id and sender_id != normalized_buyer_id:
continue
chat_buyer_nick = self._sanitize_buyer_nick(
chat_message.get('sender_name'),
source='delivery_notification_chat',
log_prefix=log_prefix,
)
if chat_buyer_nick:
return chat_buyer_nick
if normalized_buyer_id:
recent_order = db_manager.get_recent_order_by_buyer_id(
normalized_buyer_id,
cookie_id=self.cookie_id,
minutes=24 * 60,
)
if recent_order:
recent_buyer_nick = self._sanitize_buyer_nick(
recent_order.get('buyer_nick'),
source='delivery_notification_recent_order',
log_prefix=log_prefix,
)
if recent_buyer_nick:
return recent_buyer_nick
except Exception as resolve_error:
logger.warning(f"{log_prefix} 自动发货通知买家昵称解析失败: {self._safe_str(resolve_error)}")
fallback_buyer_name = self._sanitize_buyer_nick(
buyer_name,
source='delivery_notification_raw',
log_prefix=log_prefix,
)
return fallback_buyer_name or '买家'
async def send_delivery_failure_notification(
self,
send_user_name: str,
send_user_id: str,
item_id: str,
error_message: str,
chat_id: str = None,
order_id: str = None,
):
"""发送自动发货通知。"""
try:
resolved_buyer_name = self._resolve_delivery_notification_buyer_name(
send_user_name,
buyer_id=send_user_id,
chat_id=chat_id,
order_id=order_id,
log_prefix=f"{self.cookie_id}",
)
notification_message = render_notification_template(
'delivery',
account_id=self.cookie_id,
buyer_name=send_user_name,
buyer_name=resolved_buyer_name,
buyer_id=send_user_id,
item_id=item_id,
chat_id=chat_id or '未知',
+99 -8
View File
@@ -6781,6 +6781,54 @@ Cookie数量: {cookie_count}
return False
return True
def _sanitize_order_buyer_nick(self, buyer_nick: str = None) -> str:
"""过滤订单买家昵称中的系统通知标题,避免订单列表展示“工作台通知”等文案。"""
if buyer_nick is None:
return None
text = str(buyer_nick).strip()
if not text:
return None
invalid_exact_titles = {
"订单",
"全部",
"交易消息",
"等待你发货",
"买家",
"工作台通知",
"我完成了评价",
"你人真不错,送你闲鱼小红花",
"卖家人不错?送Ta闲鱼小红花",
"快给ta一个评价吧~",
"快给ta一个评价吧~",
}
if text in invalid_exact_titles:
logger.info(f"忽略系统标题型订单买家昵称: {text}")
return None
invalid_keywords = (
"小红花", "待付款", "待发货", "待刀成", "成功小刀", "闲鱼",
"交易", "收货", "退款", "评价", "发货", "付款", "拍下",
"确认", "关闭", "鼓励", "真不错", "全部", "订单",
)
if any(keyword in text for keyword in invalid_keywords):
logger.info(f"忽略系统关键词型订单买家昵称: {text}")
return None
return text
def _resolve_order_buyer_nick_for_write(self, order_id: str, buyer_nick: str = None, existing_buyer_nick: str = None) -> str:
sanitized_incoming = self._sanitize_order_buyer_nick(buyer_nick)
if sanitized_incoming:
return sanitized_incoming
sanitized_existing = self._sanitize_order_buyer_nick(existing_buyer_nick)
if sanitized_existing:
return sanitized_existing
return None
def insert_or_update_order(self, order_id: str, item_id: str = None, buyer_id: str = None,
spec_name: str = None, spec_value: str = None, quantity: str = None,
amount: str = None, order_status: str = None, cookie_id: str = None,
@@ -6824,8 +6872,10 @@ Cookie数量: {cookie_count}
return False
# 检查订单是否已存在
cursor.execute("SELECT order_id FROM orders WHERE order_id = ?", (order_id,))
cursor.execute("SELECT order_id, buyer_nick FROM orders WHERE order_id = ?", (order_id,))
existing = cursor.fetchone()
existing_buyer_nick = existing[1] if existing else None
resolved_buyer_nick = self._resolve_order_buyer_nick_for_write(order_id, buyer_nick, existing_buyer_nick)
if existing:
# 更新现有订单
@@ -6842,8 +6892,11 @@ Cookie数量: {cookie_count}
else:
logger.debug(f"跳过无效buyer_id覆盖: order_id={order_id}, invalid_buyer_id={buyer_id}")
if buyer_nick is not None:
update_fields.append("buyer_nick = ?")
update_values.append(buyer_nick)
if resolved_buyer_nick is not None:
update_fields.append("buyer_nick = ?")
update_values.append(resolved_buyer_nick)
elif existing_buyer_nick and self._sanitize_order_buyer_nick(existing_buyer_nick) is None:
update_fields.append("buyer_nick = NULL")
if sid is not None:
update_fields.append("sid = ?")
update_values.append(sid)
@@ -6907,7 +6960,7 @@ Cookie数量: {cookie_count}
'spec_name_2', 'spec_value_2', 'quantity', 'amount', 'order_status', 'cookie_id'
]
insert_values = [
order_id, item_id, sanitized_buyer_id, buyer_nick, sid, spec_name, spec_value,
order_id, item_id, sanitized_buyer_id, resolved_buyer_nick, sid, spec_name, spec_value,
spec_name_2, spec_value_2, quantity, amount, normalized_order_status or 'unknown', cookie_id
]
@@ -7002,6 +7055,37 @@ Cookie数量: {cookie_count}
logger.error(f"获取订单退款前状态失败: {order_id} - {e}")
return None
def _lookup_buyer_nick_from_chat_messages(self, cookie_id: str, sid: str = None, buyer_id: str = None) -> str:
chat_id = str(sid or '').strip().split('@')[0]
normalized_buyer_id = str(buyer_id or '').strip()
if not chat_id:
return None
try:
cursor = self.conn.cursor()
params = [cookie_id, chat_id]
buyer_filter = ''
if normalized_buyer_id:
buyer_filter = ' AND sender_id = ?'
params.append(normalized_buyer_id)
cursor.execute(f'''
SELECT sender_name
FROM chat_messages
WHERE cookie_id = ? AND chat_id = ? AND direction = 2
AND sender_name IS NOT NULL AND sender_name != ''{buyer_filter}
ORDER BY id DESC
LIMIT 80
''', params)
for row in cursor.fetchall():
buyer_nick = self._sanitize_order_buyer_nick(row[0])
if buyer_nick:
return buyer_nick
except Exception as e:
logger.debug(f"从聊天记录兜底买家昵称失败: cookie_id={cookie_id}, sid={sid}, buyer_id={buyer_id}, error={e}")
return None
def get_orders_by_cookie(self, cookie_id: str, limit: int = 100):
"""根据Cookie ID获取订单列表"""
with self.lock:
@@ -7017,11 +7101,14 @@ Cookie数量: {cookie_count}
orders = []
for row in cursor.fetchall():
buyer_nick = self._sanitize_order_buyer_nick(row[3])
if not buyer_nick:
buyer_nick = self._lookup_buyer_nick_from_chat_messages(cookie_id, row[4], row[2])
orders.append({
'order_id': row[0],
'item_id': row[1],
'buyer_id': row[2],
'buyer_nick': row[3],
'buyer_nick': buyer_nick,
'sid': row[4],
'spec_name': row[5],
'spec_value': row[6],
@@ -7082,6 +7169,10 @@ Cookie数量: {cookie_count}
if not buyer_id or not buyer_nick:
return 0
sanitized_buyer_nick = self._sanitize_order_buyer_nick(buyer_nick)
if not sanitized_buyer_nick:
return 0
with self.lock:
try:
cursor = self.conn.cursor()
@@ -7091,18 +7182,18 @@ Cookie数量: {cookie_count}
cursor.execute('''
UPDATE orders SET buyer_nick = ?
WHERE buyer_id = ? AND cookie_id = ?
''', (buyer_nick, buyer_id, cookie_id))
''', (sanitized_buyer_nick, buyer_id, cookie_id))
else:
cursor.execute('''
UPDATE orders SET buyer_nick = ?
WHERE buyer_id = ?
''', (buyer_nick, buyer_id))
''', (sanitized_buyer_nick, buyer_id))
updated_count = cursor.rowcount
self.conn.commit()
if updated_count > 0:
logger.info(f"已更新买家 {buyer_id}{updated_count} 个订单昵称为: {buyer_nick}")
logger.info(f"已更新买家 {buyer_id}{updated_count} 个订单昵称为: {sanitized_buyer_nick}")
return updated_count
+86 -14
View File
@@ -1,26 +1,98 @@
#!/bin/bash
# 闲鱼自动回复系统停止脚本
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "正在停止闲鱼自动回复系统..."
if pgrep -f "Start.py" > /dev/null; then
# 先停掉项目内由 execjs/扫码链路拉起的 node 子进程。
# 如果先杀 Pythonnode 往已关闭的 stdout/pipe 写数据时会抛 write EPIPE。
pkill -TERM -f "utils/gen_tfstk.js" 2>/dev/null || true
pkill -TERM -f "utils/et_f.js" 2>/dev/null || true
sleep 0.3
# 递归获取某个 PID 的所有子孙进程。
get_descendants() {
local parent="$1"
local children child
children=$(pgrep -P "$parent" 2>/dev/null || true)
for child in $children; do
echo "$child"
get_descendants "$child"
done
}
pkill -TERM -f "Start.py"
sleep 2
is_alive() {
kill -0 "$1" 2>/dev/null
}
if pgrep -f "Start.py" > /dev/null; then
echo "正在强制停止..."
pkill -9 -f "Start.py"
wait_pids_exit() {
local timeout_seconds="$1"
shift
local pids="$*"
local loops=$((timeout_seconds * 10))
local i pid alive
[ -z "$pids" ] && return 0
for ((i = 0; i < loops; i++)); do
alive=0
for pid in $pids; do
if is_alive "$pid"; then
alive=1
break
fi
done
[ "$alive" -eq 0 ] && return 0
sleep 0.1
done
return 1
}
unique_pids() {
printf '%s\n' "$@" | awk 'NF && !seen[$1]++'
}
# 只匹配当前项目的 Start.py,避免误杀其它同名项目。
start_pids=$(pgrep -f "${PROJECT_DIR}/Start.py" 2>/dev/null || true)
if [ -z "$start_pids" ]; then
# 兼容从项目目录内以相对路径启动的情况。
start_pids=$(pgrep -f "python.*Start.py" 2>/dev/null || true)
fi
if [ -n "$start_pids" ]; then
# 先停止 Python 主进程派生出来的 node 子进程。
# ExecJS/扫码登录链路的 node 命令行不一定包含固定 js 文件名;如果先停 Python,
# node 可能继续向已关闭的 stdout/stderr pipe 写入并打印 write EPIPE。
node_pids=""
for start_pid in $start_pids; do
descendants=$(get_descendants "$start_pid")
for pid in $descendants; do
comm=$(ps -p "$pid" -o comm= 2>/dev/null | awk '{print $1}')
args=$(ps -p "$pid" -o command= 2>/dev/null || true)
if [ "$comm" = "node" ] || echo "$args" | grep -qE '(^|/)node( |$)'; then
node_pids="$node_pids $pid"
fi
done
done
# 兜底:项目内已知的 node 脚本名。
known_node_pids=$(pgrep -f "${PROJECT_DIR}/utils/gen_tfstk.js|${PROJECT_DIR}/utils/et_f.js|utils/gen_tfstk.js|utils/et_f.js" 2>/dev/null || true)
node_pids=$(unique_pids $node_pids $known_node_pids)
if [ -n "$node_pids" ]; then
kill -TERM $node_pids 2>/dev/null || true
if ! wait_pids_exit 2 $node_pids; then
kill -KILL $node_pids 2>/dev/null || true
wait_pids_exit 1 $node_pids >/dev/null 2>&1 || true
fi
fi
# 兜底清理 lite 扫码登录可能残留的 node 子进程
pkill -9 -f "utils/gen_tfstk.js" 2>/dev/null || true
pkill -9 -f "utils/et_f.js" 2>/dev/null || true
# 再停止 Python 主进程
kill -TERM $start_pids 2>/dev/null || true
if ! wait_pids_exit 5 $start_pids; then
echo "正在强制停止..."
kill -KILL $start_pids 2>/dev/null || true
wait_pids_exit 2 $start_pids >/dev/null 2>&1 || true
fi
# 最后兜底清理启动过程中可能残留的已知 node 子进程。
pkill -KILL -f "${PROJECT_DIR}/utils/gen_tfstk.js|${PROJECT_DIR}/utils/et_f.js|utils/gen_tfstk.js|utils/et_f.js" 2>/dev/null || true
echo "已停止"
else