fix(core): 🐛 优化数据库结构并添加用户状态标记
在 Telegram Bot 核心逻辑中,为消息表增加了 topic_message_id 字段以支持 话题模式。重构了 Telegram API 请求封装逻辑,增强了错误处理能力。 同时在文档中增加了关于“用户屏蔽 Bot”的常见问题说明。系统现在可以自动 检测用户屏蔽状态,并在管理界面展示屏蔽标记,当用户重新互动时会自动清 除该标记。
This commit is contained in:
@@ -176,9 +176,8 @@ async function dbInit(env) {
|
||||
await env.TG_BOT_DB.batch([
|
||||
env.TG_BOT_DB.prepare(`CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT)`),
|
||||
env.TG_BOT_DB.prepare(`CREATE TABLE IF NOT EXISTS users (user_id TEXT PRIMARY KEY, user_state TEXT DEFAULT 'new', is_blocked INTEGER DEFAULT 0, block_count INTEGER DEFAULT 0, first_message_sent INTEGER DEFAULT 0, topic_id TEXT, user_info_json TEXT)`),
|
||||
env.TG_BOT_DB.prepare(`CREATE TABLE IF NOT EXISTS messages (user_id TEXT, message_id TEXT, text TEXT, date INTEGER, PRIMARY KEY (user_id, message_id))`)
|
||||
env.TG_BOT_DB.prepare(`CREATE TABLE IF NOT EXISTS messages (user_id TEXT, message_id TEXT, text TEXT, date INTEGER, topic_message_id TEXT, PRIMARY KEY (user_id, message_id))`)
|
||||
]);
|
||||
try { await sql(env, "ALTER TABLE messages ADD COLUMN topic_message_id TEXT"); } catch (e) {}
|
||||
}
|
||||
|
||||
// --- 4. 业务逻辑 (核心流) ---
|
||||
@@ -268,20 +267,20 @@ async function handleUserDelete(msg, u, env) {
|
||||
const targetMsgIdRaw = msg.reply_to_message.message_id;
|
||||
const targetMsgId = targetMsgIdRaw.toString();
|
||||
console.log(`Delete request: user=${u.user_id}, target_msg_raw=${targetMsgIdRaw} (type: ${typeof targetMsgIdRaw}), target_msg_str=${targetMsgId}`);
|
||||
|
||||
|
||||
// 查询对应的管理员侧消息ID - 尝试多种可能的格式
|
||||
let ref = await sql(env, "SELECT topic_message_id FROM messages WHERE user_id=? AND message_id=?", [u.user_id, targetMsgId], 'first');
|
||||
|
||||
|
||||
// 如果没找到,尝试用整数查询(以防数据库中存的是数字)
|
||||
if (!ref || !ref.topic_message_id) {
|
||||
console.log(`First query failed, trying with integer...`);
|
||||
ref = await sql(env, "SELECT topic_message_id FROM messages WHERE user_id=? AND message_id=?", [u.user_id, parseInt(targetMsgId)], 'first');
|
||||
}
|
||||
|
||||
|
||||
if (!ref || !ref.topic_message_id) {
|
||||
console.log(`Delete failed: No mapping found for user=${u.user_id}, msg=${targetMsgId}`);
|
||||
console.log(`Tip: Check database records with: SELECT * FROM messages WHERE user_id='${u.user_id}'`);
|
||||
|
||||
|
||||
// 帮助用户排查:列出该用户的最近5条消息记录
|
||||
try {
|
||||
const recentMsgs = await sql(env, "SELECT message_id, topic_message_id, text FROM messages WHERE user_id=? ORDER BY date DESC LIMIT 5", [u.user_id], 'all');
|
||||
@@ -291,7 +290,7 @@ async function handleUserDelete(msg, u, env) {
|
||||
} catch (e) {
|
||||
console.log(`Failed to fetch recent messages:`, e.message);
|
||||
}
|
||||
|
||||
|
||||
return api(env.BOT_TOKEN, "sendMessage", {
|
||||
chat_id: u.user_id,
|
||||
text: "❌ 未找到对应的消息记录,可能该消息未被转发或已被删除",
|
||||
@@ -300,14 +299,14 @@ async function handleUserDelete(msg, u, env) {
|
||||
}
|
||||
|
||||
console.log(`Delete success: Found mapping topic_msg=${ref.topic_message_id}`);
|
||||
|
||||
|
||||
try {
|
||||
// 1. 删除被回复的目标消息(先删目标,再删命令)
|
||||
await api(env.BOT_TOKEN, "deleteMessage", {
|
||||
chat_id: u.user_id,
|
||||
message_id: parseInt(targetMsgId)
|
||||
}).catch((e) => console.log("Failed to delete target msg:", e.message));
|
||||
|
||||
|
||||
// 2. 删除用户侧的 /del 命令消息
|
||||
await api(env.BOT_TOKEN, "deleteMessage", {
|
||||
chat_id: u.user_id,
|
||||
@@ -326,7 +325,7 @@ async function handleUserDelete(msg, u, env) {
|
||||
// 4. 清理数据库记录
|
||||
await sql(env, "DELETE FROM messages WHERE user_id=? AND message_id=?", [u.user_id, targetMsgId]);
|
||||
console.log(`Delete completed: Cleaned up database record`);
|
||||
|
||||
|
||||
} catch (e) {
|
||||
console.error("User Delete Failed:", e);
|
||||
await api(env.BOT_TOKEN, "sendMessage", {
|
||||
@@ -360,10 +359,10 @@ async function handleAdminDelete(msg, env, delCmd = parseDelCommand(msg.text ||
|
||||
}
|
||||
|
||||
const targetTopicMsgId = msg.reply_to_message.message_id;
|
||||
|
||||
|
||||
// 查询对应的用户侧消息ID
|
||||
const ref = await sql(env, "SELECT user_id, message_id FROM messages WHERE topic_message_id=?", targetTopicMsgId.toString(), 'first');
|
||||
|
||||
|
||||
if (!ref) {
|
||||
return api(env.BOT_TOKEN, "sendMessage", {
|
||||
chat_id: msg.chat.id,
|
||||
@@ -378,7 +377,7 @@ async function handleAdminDelete(msg, env, delCmd = parseDelCommand(msg.text ||
|
||||
chat_id: msg.chat.id,
|
||||
message_id: msg.message_id
|
||||
}).catch(() => {});
|
||||
|
||||
|
||||
await api(env.BOT_TOKEN, "deleteMessage", {
|
||||
chat_id: msg.chat.id,
|
||||
message_id: targetTopicMsgId
|
||||
@@ -392,7 +391,7 @@ async function handleAdminDelete(msg, env, delCmd = parseDelCommand(msg.text ||
|
||||
|
||||
// 3. 清理数据库记录
|
||||
await sql(env, "DELETE FROM messages WHERE user_id=? AND message_id=?", [ref.user_id, ref.message_id]);
|
||||
|
||||
|
||||
} catch (e) {
|
||||
console.error("Admin Delete Failed:", e);
|
||||
await api(env.BOT_TOKEN, "sendMessage", {
|
||||
@@ -649,6 +648,31 @@ async function sendStart(id, msg, env) {
|
||||
// 正常态用户消息防线:敏感词与类型拦截器
|
||||
async function handleVerifiedMsg(msg, u, env) {
|
||||
const id = u.user_id, text = msg.text || "";
|
||||
|
||||
// 如果用户之前被标记为屏蔽 Bot,但现在能发消息,说明已解除屏蔽
|
||||
if (u.user_info && u.user_info.bot_blocked) {
|
||||
u.user_info.bot_blocked = false;
|
||||
delete u.user_info.bot_blocked_ts;
|
||||
await updUser(id, { user_info: u.user_info }, env);
|
||||
console.log(`Cleared bot_blocked mark for user ${id} after receiving message`);
|
||||
|
||||
// 更新用户卡片显示
|
||||
try {
|
||||
if (u.topic_id && u.user_info.card_msg_id) {
|
||||
const mockTgUser = { id: id, username: u.user_info.username || "", first_name: u.user_info.name || "(未获取)", last_name: "" };
|
||||
const newMeta = getUMeta(mockTgUser, u, u.user_info.join_date || (Date.now()/1000));
|
||||
await api(env.BOT_TOKEN, "editMessageCaption", {
|
||||
chat_id: env.ADMIN_GROUP_ID,
|
||||
message_id: u.user_info.card_msg_id,
|
||||
caption: newMeta.card,
|
||||
parse_mode: "HTML",
|
||||
reply_markup: getBtns(id, u.is_blocked, newMeta.username)
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to update card after bot_blocked cleared:", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 敏感词屏蔽预检系统
|
||||
if (text) {
|
||||
@@ -1072,17 +1096,49 @@ async function handleAdminReply(msg, env) {
|
||||
}
|
||||
}
|
||||
|
||||
const uid = (await sql(env, "SELECT user_id FROM users WHERE topic_id = ?", msg.message_thread_id.toString(), 'first'))?.user_id;
|
||||
if (!uid) return;
|
||||
const topicIdStr = msg.message_thread_id.toString();
|
||||
console.log(`Admin reply debug: topic_id=${topicIdStr}, admin_msg_id=${msg.message_id}`);
|
||||
|
||||
const userRef = await sql(env, "SELECT user_id FROM users WHERE topic_id = ?", topicIdStr, 'first');
|
||||
console.log(`Admin reply debug: userRef=`, userRef);
|
||||
|
||||
const uid = userRef?.user_id;
|
||||
if (!uid) {
|
||||
console.error(`Admin reply failed: No user found for topic_id=${topicIdStr}`);
|
||||
return api(env.BOT_TOKEN, "sendMessage", {
|
||||
chat_id: msg.chat.id,
|
||||
message_thread_id: msg.message_thread_id,
|
||||
text: `❌ 系统错误:未找到关联的用户(topic_id=${topicIdStr})`
|
||||
});
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
const u = await getUser(uid, env);
|
||||
console.log(`Admin reply debug: user_state=${u.user_state}, is_blocked=${u.is_blocked}`);
|
||||
|
||||
if (u.is_blocked) {
|
||||
return api(env.BOT_TOKEN, "sendMessage", {
|
||||
chat_id: msg.chat.id,
|
||||
message_thread_id: msg.message_thread_id,
|
||||
text: `❌ 该用户已被屏蔽,无法发送消息`
|
||||
});
|
||||
}
|
||||
|
||||
let replyToMsgId = undefined;
|
||||
if (msg.reply_to_message) {
|
||||
const ref = await sql(env, "SELECT message_id FROM messages WHERE topic_message_id = ?", msg.reply_to_message.message_id.toString(), 'first');
|
||||
if (ref) replyToMsgId = ref.message_id;
|
||||
if (ref) {
|
||||
replyToMsgId = ref.message_id;
|
||||
console.log(`Admin reply debug: Found reply mapping topic_msg=${msg.reply_to_message.message_id} -> user_msg=${replyToMsgId}`);
|
||||
} else {
|
||||
console.log(`Admin reply debug: No reply mapping found for topic_msg=${msg.reply_to_message.message_id}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Admin reply debug: Delivering message to uid=${uid}, replyToMsgId=${replyToMsgId}`);
|
||||
const sent = await deliverAdminMessageToUser(msg, uid, replyToMsgId, env);
|
||||
console.log(`Admin reply debug: Delivery success, sent.message_id=${sent?.message_id}`);
|
||||
if (sent && sent.message_id) {
|
||||
const storeText = msg.text || msg.caption || "[Admin Message]";
|
||||
await sql(env, "INSERT OR REPLACE INTO messages (user_id, message_id, text, date, topic_message_id) VALUES (?,?,?,?,?)",
|
||||
@@ -1091,11 +1147,61 @@ async function handleAdminReply(msg, env) {
|
||||
// 此处为管理员端给用户下发消息的主逻辑。根据之前的版本,管理员侧发送成功后的回执代码也已经去除
|
||||
} catch (e) {
|
||||
console.error("Admin Delivery Failed:", e);
|
||||
await api(env.BOT_TOKEN, "sendMessage", {
|
||||
chat_id: msg.chat.id,
|
||||
message_thread_id: msg.message_thread_id,
|
||||
text: `❌ 内部投递失败:${e.message || "Unknown error"}`
|
||||
console.error("Admin Delivery Failed - Error details:", {
|
||||
message: e.message,
|
||||
stack: e.stack,
|
||||
uid: uid,
|
||||
topicId: topicIdStr,
|
||||
replyToMsgId: replyToMsgId
|
||||
});
|
||||
|
||||
// 检测用户是否屏蔽了 Bot
|
||||
const isBlocked = e.message && e.message.includes("bot was blocked by the user");
|
||||
|
||||
if (isBlocked) {
|
||||
// 自动标记用户为屏蔽状态
|
||||
try {
|
||||
const u = await getUser(uid, env);
|
||||
if (!u.user_info.bot_blocked) {
|
||||
u.user_info.bot_blocked = true;
|
||||
u.user_info.bot_blocked_ts = Date.now();
|
||||
await updUser(uid, { user_info: u.user_info }, env);
|
||||
console.log(`Auto-marked user ${uid} as bot_blocked`);
|
||||
|
||||
// 更新用户卡片显示
|
||||
if (u.topic_id && u.user_info.card_msg_id) {
|
||||
const mockTgUser = { id: uid, username: u.user_info.username || "", first_name: u.user_info.name || "(未获取)", last_name: "" };
|
||||
const newMeta = getUMeta(mockTgUser, u, u.user_info.join_date || (Date.now()/1000));
|
||||
try {
|
||||
await api(env.BOT_TOKEN, "editMessageCaption", {
|
||||
chat_id: env.ADMIN_GROUP_ID,
|
||||
message_id: u.user_info.card_msg_id,
|
||||
caption: newMeta.card,
|
||||
parse_mode: "HTML",
|
||||
reply_markup: getBtns(uid, u.is_blocked, newMeta.username)
|
||||
});
|
||||
} catch (editErr) {
|
||||
console.log("Failed to update card after bot_blocked detection:", editErr.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (markErr) {
|
||||
console.error("Failed to mark user as bot_blocked:", markErr);
|
||||
}
|
||||
|
||||
await api(env.BOT_TOKEN, "sendMessage", {
|
||||
chat_id: msg.chat.id,
|
||||
message_thread_id: msg.message_thread_id,
|
||||
text: `⚠️ <b>用户已屏蔽 Bot</b>\n\n该用户已在 Telegram 中屏蔽了本机器人,无法接收消息。\n\n请通过其他方式联系用户,让其:\n1️⃣ 打开与机器人的聊天\n2️⃣ 解除屏蔽\n3️⃣ 重新发送 /start`,
|
||||
parse_mode: "HTML"
|
||||
});
|
||||
} else {
|
||||
await api(env.BOT_TOKEN, "sendMessage", {
|
||||
chat_id: msg.chat.id,
|
||||
message_thread_id: msg.message_thread_id,
|
||||
text: `❌ 内部投递失败:${e.message || "Unknown error"}\n\n调试信息:\n用户ID: ${uid}\n话题ID: ${topicIdStr}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1107,7 +1213,7 @@ async function handleEdit(msg, env) {
|
||||
const newTxt = msg.text || msg.caption || "[非文本]";
|
||||
|
||||
const logText = `✏️ 消息修改\n前: ${escape(old?.text||"?")}\n后: ${escape(newTxt)}`;
|
||||
|
||||
yixia
|
||||
await api(env.BOT_TOKEN, "sendMessage", {
|
||||
chat_id: env.ADMIN_GROUP_ID,
|
||||
message_thread_id: u.topic_id,
|
||||
@@ -1223,6 +1329,15 @@ async function handleCallback(cb, env) {
|
||||
else if (['block','unblock'].includes(act)) {
|
||||
const isB = act === 'block'; const uid = p1; const u = await getUser(uid, env); const bid = await getCfg('blocked_topic_id', env);
|
||||
await updUser(uid, { is_blocked: isB, block_count: 0 }, env);
|
||||
|
||||
// 如果是解封操作,清除 bot_blocked 标记
|
||||
if (!isB && u.user_info.bot_blocked) {
|
||||
u.user_info.bot_blocked = false;
|
||||
delete u.user_info.bot_blocked_ts;
|
||||
await updUser(uid, { user_info: u.user_info }, env);
|
||||
console.log(`Cleared bot_blocked mark for user ${uid} after admin unblock`);
|
||||
}
|
||||
|
||||
// 响应变更,刷新目标人员资料卡片上的按钮渲染状态
|
||||
if (u.user_info.card_msg_id) {
|
||||
api(env.BOT_TOKEN, "editMessageReplyMarkup", { chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.card_msg_id, reply_markup: getBtns(uid, isB, u.user_info.username) }).catch(()=>{});
|
||||
@@ -1434,13 +1549,23 @@ const getUMeta = (tgUser, dbUser, d) => {
|
||||
const note = dbUser.user_info && dbUser.user_info.note ? `\n📝 <b>附加备注:</b> ${escape(dbUser.user_info.note)}` : "";
|
||||
const labelDisplay = tgUser.username ? `@${tgUser.username}` : "未设公开ID";
|
||||
const timeStr = new Date(d*1000).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
|
||||
|
||||
// 检测并显示屏蔽状态
|
||||
let blockStatus = "";
|
||||
if (dbUser.is_blocked) {
|
||||
blockStatus = `\n🚫 <b>管理员屏蔽:</b> 是`;
|
||||
}
|
||||
if (dbUser.user_info && dbUser.user_info.bot_blocked) {
|
||||
const blockTime = new Date(dbUser.user_info.bot_blocked_ts || d*1000).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
|
||||
blockStatus += `\n⛔ <b>用户屏蔽Bot:</b> 是 (${blockTime})`;
|
||||
}
|
||||
|
||||
return {
|
||||
userId: id,
|
||||
name,
|
||||
username: tgUser.username,
|
||||
topicName: name.substring(0, 128),
|
||||
card: `<b>🪪 用户身份卡片</b>\n---\n👤: ${safeName}\n🏷️: ${labelDisplay}\n🆔: <code>${id}</code>${note}\n🕒: <code>${timeStr}</code>`
|
||||
card: `<b>🪪 用户身份卡片</b>\n---\n👤: ${safeName}\n🏷️: ${labelDisplay}\n🆔: <code>${id}</code>${note}${blockStatus}\n🕒: <code>${timeStr}</code>`
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1489,4 +1614,4 @@ const getBtns = (id, blk, username) => {
|
||||
btns.push([{ text: "✏️ 录入备注", callback_data: `note:set:${id}` }, { text: "📌 提升置顶", callback_data: `pin_card:${id}` }]);
|
||||
|
||||
return { inline_keyboard: btns };
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user