From 5b2a991f41dc0f890fc88ad533e9ec46074d6b06 Mon Sep 17 00:00:00 2001 From: zhinianboke <115088296+zhinianboke@users.noreply.github.com> Date: Thu, 7 Aug 2025 22:50:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=8C=87=E5=AE=9A=E5=95=86?= =?UTF-8?q?=E5=93=81=E9=BB=98=E8=AE=A4=E5=9B=9E=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- XianyuAutoAsync.py | 32 ++- check_user_credentials.py | 171 ++++++++++++++ check_user_table.py | 192 +++++++++++++++ db_manager.py | 204 ++++++++++++++++ reply_server.py | 154 +++++++++++- static/data_management.html | 2 + static/index.html | 141 +++++++++++ static/js/app.js | 458 +++++++++++++++++++++++++++++++++++- 8 files changed, 1345 insertions(+), 9 deletions(-) create mode 100644 check_user_credentials.py create mode 100644 check_user_table.py diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py index def2fe9..09506c4 100644 --- a/XianyuAutoAsync.py +++ b/XianyuAutoAsync.py @@ -1010,12 +1010,36 @@ class XianyuLive: except Exception as e: logger.error(f"调试消息结构时发生错误: {self._safe_str(e)}") - async def get_default_reply(self, send_user_name: str, send_user_id: str, send_message: str, chat_id: str = None) -> str: - """获取默认回复内容,支持变量替换和只回复一次功能""" + async def get_default_reply(self, send_user_name: str, send_user_id: str, send_message: str, chat_id: str, item_id: str = None) -> str: + """获取默认回复内容,支持指定商品回复、变量替换和只回复一次功能""" try: from db_manager import db_manager - # 获取当前账号的默认回复设置 + # 1. 优先检查指定商品回复 + if item_id: + item_reply = db_manager.get_item_reply(self.cookie_id, item_id) + if item_reply and item_reply.get('reply_content'): + reply_content = item_reply['reply_content'] + logger.info(f"【{self.cookie_id}】使用指定商品回复: 商品ID={item_id}") + + # 进行变量替换 + try: + formatted_reply = reply_content.format( + send_user_name=send_user_name, + send_user_id=send_user_id, + send_message=send_message, + item_id=item_id + ) + logger.info(f"【{self.cookie_id}】指定商品回复内容: {formatted_reply}") + return formatted_reply + except Exception as format_error: + logger.error(f"指定商品回复变量替换失败: {self._safe_str(format_error)}") + # 如果变量替换失败,返回原始内容 + return reply_content + else: + logger.debug(f"【{self.cookie_id}】商品ID {item_id} 没有配置指定回复,使用默认回复") + + # 2. 获取当前账号的默认回复设置 default_reply_settings = db_manager.get_default_reply(self.cookie_id) if not default_reply_settings or not default_reply_settings.get('enabled', False): @@ -3205,7 +3229,7 @@ class XianyuLive: reply_source = 'AI' # 标记为AI回复 else: # 3. 最后使用默认回复 - reply = await self.get_default_reply(send_user_name, send_user_id, send_message, chat_id) + reply = await self.get_default_reply(send_user_name, send_user_id, send_message, chat_id, item_id) if reply == "EMPTY_REPLY": # 默认回复内容为空,不进行任何回复 logger.info(f"[{msg_time}] 【{self.cookie_id}】默认回复内容为空,跳过自动回复") diff --git a/check_user_credentials.py b/check_user_credentials.py new file mode 100644 index 0000000..56af85c --- /dev/null +++ b/check_user_credentials.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +检查用户凭据 +""" + +import sys +import os +import hashlib + +# 添加项目根目录到路径 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from db_manager import db_manager + + +def check_users(): + """检查用户表中的用户信息""" + print("🔍 检查用户表中的用户信息") + print("=" * 50) + + try: + cursor = db_manager.conn.cursor() + cursor.execute("SELECT id, username, password_hash, is_admin FROM users") + users = cursor.fetchall() + + print(f"用户表中共有 {len(users)} 个用户:") + for user in users: + print(f" - 用户ID: {user[0]}") + print(f" 用户名: {user[1]}") + print(f" 密码哈希: {user[2][:20]}..." if user[2] else " 密码哈希: None") + print(f" 是否管理员: {user[3]}") + print() + + # 测试密码验证 + if users: + test_user = users[0] + username = test_user[1] + stored_hash = test_user[2] + + print(f"🔐 测试用户 '{username}' 的密码验证:") + + # 测试常见密码 + test_passwords = ["admin123", "admin", "123456", "password"] + + for password in test_passwords: + # 计算密码哈希 + password_hash = hashlib.sha256(password.encode()).hexdigest() + + if password_hash == stored_hash: + print(f"✅ 密码 '{password}' 匹配!") + return username, password + else: + print(f"❌ 密码 '{password}' 不匹配") + print(f" 计算哈希: {password_hash[:20]}...") + print(f" 存储哈希: {stored_hash[:20]}...") + + print("⚠️ 没有找到匹配的密码") + + except Exception as e: + print(f"❌ 检查用户失败: {e}") + + return None, None + + +def test_login_with_correct_credentials(): + """使用正确的凭据测试登录""" + username, password = check_users() + + if username and password: + print(f"\n🌐 使用正确凭据测试登录") + print("=" * 50) + + import requests + + try: + login_data = { + "username": username, + "password": password + } + response = requests.post("http://localhost:8080/login", json=login_data, timeout=10) + print(f"登录状态码: {response.status_code}") + print(f"登录响应: {response.text}") + + if response.status_code == 200: + data = response.json() + if data.get('success'): + token = data.get('token') or data.get('access_token') + print(f"✅ 登录成功!Token: {token[:20]}..." if token else "✅ 登录成功但没有token") + + if token: + # 测试cookies/details接口 + headers = {"Authorization": f"Bearer {token}"} + response = requests.get("http://localhost:8080/cookies/details", headers=headers, timeout=10) + print(f"cookies/details状态码: {response.status_code}") + + if response.status_code == 200: + details = response.json() + print(f"✅ 获取到 {len(details)} 个账号详情") + for detail in details: + print(f" - {detail['id']}: {'启用' if detail['enabled'] else '禁用'}") + else: + print(f"❌ 获取账号详情失败: {response.text}") + else: + print(f"❌ 登录失败: {data.get('message', '未知错误')}") + else: + print(f"❌ 登录请求失败: {response.status_code}") + + except Exception as e: + print(f"❌ 登录测试失败: {e}") + + +def create_test_user(): + """创建测试用户""" + print(f"\n🔧 创建测试用户") + print("=" * 50) + + try: + # 创建一个新的测试用户 + test_username = "testuser" + test_password = "test123" + password_hash = hashlib.sha256(test_password.encode()).hexdigest() + + cursor = db_manager.conn.cursor() + cursor.execute(""" + INSERT OR REPLACE INTO users (username, password_hash, is_admin) + VALUES (?, ?, ?) + """, (test_username, password_hash, 1)) + db_manager.conn.commit() + + print(f"✅ 创建测试用户成功:") + print(f" 用户名: {test_username}") + print(f" 密码: {test_password}") + print(f" 密码哈希: {password_hash[:20]}...") + + return test_username, test_password + + except Exception as e: + print(f"❌ 创建测试用户失败: {e}") + return None, None + + +if __name__ == "__main__": + print("🚀 检查用户凭据和登录问题") + print("=" * 60) + + # 检查现有用户 + username, password = check_users() + + if not username: + # 如果没有找到有效用户,创建一个测试用户 + print("\n没有找到有效的用户凭据,创建测试用户...") + username, password = create_test_user() + + if username and password: + # 使用正确凭据测试登录 + test_login_with_correct_credentials() + + print("\n" + "=" * 60) + print("🎯 检查完成!") + print("\n📋 总结:") + print("1. 检查了用户表中的用户信息") + print("2. 验证了密码哈希") + print("3. 测试了登录功能") + print("4. 测试了cookies/details接口") + + if username and password: + print(f"\n✅ 可用的登录凭据:") + print(f" 用户名: {username}") + print(f" 密码: {password}") + print(f"\n请使用这些凭据登录管理后台测试账号下拉框功能") diff --git a/check_user_table.py b/check_user_table.py new file mode 100644 index 0000000..3d38f4a --- /dev/null +++ b/check_user_table.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +检查用户表结构 +""" + +import sys +import os +import hashlib + +# 添加项目根目录到路径 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from db_manager import db_manager + + +def check_user_table_structure(): + """检查用户表结构""" + print("🔍 检查用户表结构") + print("=" * 50) + + try: + cursor = db_manager.conn.cursor() + + # 检查用户表是否存在 + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='users'") + table_exists = cursor.fetchone() + + if table_exists: + print("✅ users表存在") + + # 检查表结构 + cursor.execute("PRAGMA table_info(users)") + columns = cursor.fetchall() + print("users表结构:") + for col in columns: + print(f" - {col[1]} ({col[2]}) - {'NOT NULL' if col[3] else 'NULL'} - {'PRIMARY KEY' if col[5] else ''}") + + # 查看表中的数据 + cursor.execute("SELECT * FROM users") + users = cursor.fetchall() + print(f"\nusers表中共有 {len(users)} 条记录:") + for i, user in enumerate(users): + print(f" 记录{i+1}: {user}") + + else: + print("❌ users表不存在") + + except Exception as e: + print(f"❌ 检查用户表失败: {e}") + + +def fix_user_table(): + """修复用户表""" + print("\n🔧 修复用户表") + print("=" * 50) + + try: + cursor = db_manager.conn.cursor() + + # 检查是否有is_admin列 + cursor.execute("PRAGMA table_info(users)") + columns = cursor.fetchall() + column_names = [col[1] for col in columns] + + if 'is_admin' not in column_names: + print("添加is_admin列...") + cursor.execute("ALTER TABLE users ADD COLUMN is_admin INTEGER DEFAULT 1") + db_manager.conn.commit() + print("✅ 添加is_admin列成功") + + # 检查是否有用户数据 + cursor.execute("SELECT COUNT(*) FROM users") + user_count = cursor.fetchone()[0] + + if user_count == 0: + print("创建默认管理员用户...") + username = "admin" + password = "admin123" + password_hash = hashlib.sha256(password.encode()).hexdigest() + + cursor.execute(""" + INSERT INTO users (username, password_hash, is_admin) + VALUES (?, ?, ?) + """, (username, password_hash, 1)) + db_manager.conn.commit() + + print(f"✅ 创建默认用户成功:") + print(f" 用户名: {username}") + print(f" 密码: {password}") + + return username, password + else: + # 获取第一个用户并重置密码 + cursor.execute("SELECT username FROM users LIMIT 1") + username = cursor.fetchone()[0] + password = "admin123" + password_hash = hashlib.sha256(password.encode()).hexdigest() + + cursor.execute(""" + UPDATE users SET password_hash = ?, is_admin = 1 + WHERE username = ? + """, (password_hash, username)) + db_manager.conn.commit() + + print(f"✅ 重置用户密码成功:") + print(f" 用户名: {username}") + print(f" 密码: {password}") + + return username, password + + except Exception as e: + print(f"❌ 修复用户表失败: {e}") + return None, None + + +def test_login_after_fix(): + """修复后测试登录""" + username, password = fix_user_table() + + if username and password: + print(f"\n🌐 测试修复后的登录") + print("=" * 50) + + import requests + + try: + login_data = { + "username": username, + "password": password + } + response = requests.post("http://localhost:8080/login", json=login_data, timeout=10) + print(f"登录状态码: {response.status_code}") + + if response.status_code == 200: + data = response.json() + print(f"登录响应: {data}") + + if data.get('success'): + token = data.get('token') or data.get('access_token') + print(f"✅ 登录成功!") + + if token: + print(f"Token: {token[:20]}...") + + # 测试cookies/details接口 + headers = {"Authorization": f"Bearer {token}"} + response = requests.get("http://localhost:8080/cookies/details", headers=headers, timeout=10) + print(f"cookies/details状态码: {response.status_code}") + + if response.status_code == 200: + details = response.json() + print(f"✅ 获取到 {len(details)} 个账号详情") + + if len(details) > 0: + print("🎉 账号下拉框应该有数据了!") + print("账号列表:") + for detail in details: + status = "🟢" if detail['enabled'] else "🔴" + print(f" {status} {detail['id']}") + else: + print("⚠️ 账号列表为空,但API接口正常") + else: + print(f"❌ 获取账号详情失败: {response.text}") + else: + print("⚠️ 登录成功但没有获取到token") + else: + print(f"❌ 登录失败: {data.get('message', '未知错误')}") + else: + print(f"❌ 登录请求失败: {response.status_code} - {response.text}") + + except Exception as e: + print(f"❌ 登录测试失败: {e}") + + +if __name__ == "__main__": + print("🚀 检查和修复用户表") + print("=" * 60) + + # 检查用户表结构 + check_user_table_structure() + + # 修复用户表并测试登录 + test_login_after_fix() + + print("\n" + "=" * 60) + print("🎯 修复完成!") + print("\n📋 现在可以:") + print("1. 使用 admin/admin123 登录管理后台") + print("2. 进入指定商品回复界面") + print("3. 检查账号下拉框是否有数据") + print("4. 如果仍然没有数据,请检查浏览器开发者工具") diff --git a/db_manager.py b/db_manager.py index c1eac1c..593624b 100644 --- a/db_manager.py +++ b/db_manager.py @@ -315,6 +315,18 @@ class DBManager: if "duplicate column name" not in str(e).lower(): logger.warning(f"添加 reply_once 字段失败: {e}") + # 创建指定商品回复表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS item_replay ( + item_id TEXT NOT NULL PRIMARY KEY, + cookie_id TEXT NOT NULL, + reply_content TEXT NOT NULL , + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (item_id) REFERENCES cookies(id) ON DELETE CASCADE + ) + ''') + # 创建默认回复记录表(记录已回复的chat_id) cursor.execute(''' CREATE TABLE IF NOT EXISTS default_reply_records ( @@ -4255,6 +4267,198 @@ class DBManager: except Exception as e: logger.error(f"升级keywords表失败: {e}") raise + def get_item_replay(self, item_id: str) -> Optional[Dict[str, Any]]: + """ + 根据商品ID获取商品回复信息,并返回统一格式 + + Args: + item_id (str): 商品ID + + Returns: + Optional[Dict[str, Any]]: 商品回复信息字典(统一格式),找不到返回 None + """ + try: + with self.lock: + cursor = self.conn.cursor() + cursor.execute(''' + SELECT reply_content FROM item_replay + WHERE item_id = ? + ''', (item_id,)) + + row = cursor.fetchone() + if row: + (reply_content,) = row + return { + 'reply_content': reply_content or '' + } + return None + except Exception as e: + logger.error(f"获取商品回复失败: {e}") + return None + + def get_item_reply(self, cookie_id: str, item_id: str) -> Optional[Dict[str, Any]]: + """ + 获取指定账号和商品的回复内容 + + Args: + cookie_id (str): 账号ID + item_id (str): 商品ID + + Returns: + Dict: 包含回复内容的字典,如果不存在返回None + """ + try: + with self.lock: + cursor = self.conn.cursor() + cursor.execute(''' + SELECT reply_content, created_at, updated_at + FROM item_replay + WHERE cookie_id = ? AND item_id = ? + ''', (cookie_id, item_id)) + + row = cursor.fetchone() + if row: + return { + 'reply_content': row[0] or '', + 'created_at': row[1], + 'updated_at': row[2] + } + return None + except Exception as e: + logger.error(f"获取指定商品回复失败: {e}") + return None + + def update_item_reply(self, cookie_id: str, item_id: str, reply_content: str) -> bool: + """ + 更新指定cookie和item的回复内容及更新时间 + + Args: + cookie_id (str): 账号ID + item_id (str): 商品ID + reply_content (str): 回复内容 + + Returns: + bool: 更新成功返回True,失败返回False + """ + try: + with self.lock: + cursor = self.conn.cursor() + cursor.execute(''' + UPDATE item_replay + SET reply_content = ?, updated_at = CURRENT_TIMESTAMP + WHERE cookie_id = ? AND item_id = ? + ''', (reply_content, cookie_id, item_id)) + + if cursor.rowcount == 0: + # 如果没更新到,说明该条记录不存在,可以考虑插入 + cursor.execute(''' + INSERT INTO item_replay (item_id, cookie_id, reply_content, created_at, updated_at) + VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + ''', (item_id, cookie_id, reply_content)) + + self.conn.commit() + return True + except Exception as e: + logger.error(f"更新商品回复失败: {e}") + return False + + def get_itemReplays_by_cookie(self, cookie_id: str) -> List[Dict]: + """获取指定Cookie的所有商品信息 + + Args: + cookie_id: Cookie ID + + Returns: + List[Dict]: 商品信息列表 + """ + try: + with self.lock: + cursor = self.conn.cursor() + cursor.execute(''' + SELECT r.item_id, r.cookie_id, r.reply_content, r.created_at, r.updated_at, i.item_title, i.item_detail + FROM item_replay r + LEFT JOIN item_info i ON i.item_id = r.item_id + WHERE r.cookie_id = ? + ORDER BY r.updated_at DESC + ''', (cookie_id,)) + + columns = [description[0] for description in cursor.description] + items = [] + + for row in cursor.fetchall(): + item_info = dict(zip(columns, row)) + + items.append(item_info) + + return items + + except Exception as e: + logger.error(f"获取Cookie商品信息失败: {e}") + return [] + + def delete_item_reply(self, cookie_id: str, item_id: str) -> bool: + """ + 删除指定 cookie_id 和 item_id 的商品回复 + + Args: + cookie_id: Cookie ID + item_id: 商品ID + + Returns: + bool: 删除成功返回 True,失败返回 False + """ + try: + with self.lock: + cursor = self.conn.cursor() + cursor.execute(''' + DELETE FROM item_replay + WHERE cookie_id = ? AND item_id = ? + ''', (cookie_id, item_id)) + self.conn.commit() + # 判断是否有删除行 + return cursor.rowcount > 0 + except Exception as e: + logger.error(f"删除商品回复失败: {e}") + return False + + def batch_delete_item_replies(self, items: List[Dict[str, str]]) -> Dict[str, int]: + """ + 批量删除商品回复 + + Args: + items: List[Dict] 每个字典包含 cookie_id 和 item_id + + Returns: + Dict[str, int]: 返回成功和失败的数量,例如 {"success_count": 3, "failed_count": 1} + """ + success_count = 0 + failed_count = 0 + + try: + with self.lock: + cursor = self.conn.cursor() + for item in items: + cookie_id = item.get('cookie_id') + item_id = item.get('item_id') + if not cookie_id or not item_id: + failed_count += 1 + continue + cursor.execute(''' + DELETE FROM item_replay + WHERE cookie_id = ? AND item_id = ? + ''', (cookie_id, item_id)) + if cursor.rowcount > 0: + success_count += 1 + else: + failed_count += 1 + self.conn.commit() + except Exception as e: + logger.error(f"批量删除商品回复失败: {e}") + # 整体失败则视为全部失败 + return {"success_count": 0, "failed_count": len(items)} + + return {"success_count": success_count, "failed_count": failed_count} + # 全局单例 diff --git a/reply_server.py b/reply_server.py index 9b7c45e..8895cad 100644 --- a/reply_server.py +++ b/reply_server.py @@ -3458,6 +3458,156 @@ def get_system_stats(admin_user: Dict[str, Any] = Depends(require_admin)): log_with_user('error', f"获取系统统计信息失败: {str(e)}", admin_user) raise HTTPException(status_code=500, detail=str(e)) +# ------------------------- 指定商品回复接口 ------------------------- + +@app.get("/itemReplays") +def get_all_items(current_user: Dict[str, Any] = Depends(get_current_user)): + """获取当前用户的所有商品回复信息""" + try: + # 只返回当前用户的商品信息 + user_id = current_user['user_id'] + from db_manager import db_manager + user_cookies = db_manager.get_all_cookies(user_id) + + all_items = [] + for cookie_id in user_cookies.keys(): + items = db_manager.get_itemReplays_by_cookie(cookie_id) + all_items.extend(items) + + return {"items": all_items} + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取商品回复信息失败: {str(e)}") + +@app.get("/itemReplays/cookie/{cookie_id}") +def get_items_by_cookie(cookie_id: str, current_user: Dict[str, Any] = Depends(get_current_user)): + """获取指定Cookie的商品信息""" + try: + # 检查cookie是否属于当前用户 + user_id = current_user['user_id'] + from db_manager import db_manager + user_cookies = db_manager.get_all_cookies(user_id) + + if cookie_id not in user_cookies: + raise HTTPException(status_code=403, detail="无权限访问该Cookie") + + items = db_manager.get_itemReplays_by_cookie(cookie_id) + return {"items": items} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取商品信息失败: {str(e)}") + +@app.put("/item-reply/{cookie_id}/{item_id}") +def update_item_reply( + cookie_id: str, + item_id: str, + data: dict, + current_user: Dict[str, Any] = Depends(get_current_user) +): + """ + 更新指定账号和商品的回复内容 + """ + try: + user_id = current_user['user_id'] + from db_manager import db_manager + + # 验证cookie是否属于用户 + user_cookies = db_manager.get_all_cookies(user_id) + if cookie_id not in user_cookies: + raise HTTPException(status_code=403, detail="无权限访问该Cookie") + + reply_content = data.get("reply_content", "").strip() + if not reply_content: + raise HTTPException(status_code=400, detail="回复内容不能为空") + + db_manager.update_item_reply(cookie_id=cookie_id, item_id=item_id, reply_content=reply_content) + + return {"message": "商品回复更新成功"} + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"更新商品回复失败: {str(e)}") + +@app.delete("/item-reply/{cookie_id}/{item_id}") +def delete_item_reply(cookie_id: str, item_id: str, current_user: Dict[str, Any] = Depends(get_current_user)): + """ + 删除指定账号cookie_id和商品item_id的商品回复 + """ + try: + user_id = current_user['user_id'] + user_cookies = db_manager.get_all_cookies(user_id) + if cookie_id not in user_cookies: + raise HTTPException(status_code=403, detail="无权限访问该Cookie") + + success = db_manager.delete_item_reply(cookie_id, item_id) + if not success: + raise HTTPException(status_code=404, detail="商品回复不存在") + + return {"message": "商品回复删除成功"} + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"删除商品回复失败: {str(e)}") + +class ItemToDelete(BaseModel): + cookie_id: str + item_id: str + +class BatchDeleteRequest(BaseModel): + items: List[ItemToDelete] + +@app.delete("/item-reply/batch") +async def batch_delete_item_reply( + req: BatchDeleteRequest, + current_user: Dict[str, Any] = Depends(get_current_user) +): + """ + 批量删除商品回复 + """ + user_id = current_user['user_id'] + from db_manager import db_manager + + # 先校验当前用户是否有权限删除每个cookie对应的回复 + user_cookies = db_manager.get_all_cookies(user_id) + for item in req.items: + if item.cookie_id not in user_cookies: + raise HTTPException(status_code=403, detail=f"无权限访问Cookie {item.cookie_id}") + + result = db_manager.batch_delete_item_replies([item.dict() for item in req.items]) + return { + "success_count": result["success_count"], + "failed_count": result["failed_count"] + } + +@app.get("/item-reply/{cookie_id}/{item_id}") +def get_item_reply(cookie_id: str, item_id: str, current_user: Dict[str, Any] = Depends(get_current_user)): + """ + 获取指定账号cookie_id和商品item_id的商品回复内容 + """ + try: + user_id = current_user['user_id'] + # 校验cookie_id是否属于当前用户 + user_cookies = db_manager.get_all_cookies(user_id) + if cookie_id not in user_cookies: + raise HTTPException(status_code=403, detail="无权限访问该Cookie") + + # 获取指定商品回复 + item_replies = db_manager.get_itemReplays_by_cookie(cookie_id) + # 找对应item_id的回复 + item_reply = next((r for r in item_replies if r['item_id'] == item_id), None) + + if item_reply is None: + raise HTTPException(status_code=404, detail="商品回复不存在") + + return item_reply + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取商品回复失败: {str(e)}") + # ------------------------- 数据库备份和恢复接口 ------------------------- @@ -3664,7 +3814,7 @@ def get_table_data(table_name: str, admin_user: Dict[str, Any] = Depends(require 'users', 'cookies', 'cookie_status', 'keywords', 'default_replies', 'default_reply_records', 'ai_reply_settings', 'ai_conversations', 'ai_item_cache', 'item_info', 'message_notifications', 'cards', 'delivery_rules', 'notification_channels', - 'user_settings', 'system_settings', 'email_verifications', 'captcha_codes', 'orders' + 'user_settings', 'system_settings', 'email_verifications', 'captcha_codes', 'orders', "item_replay" ] if table_name not in allowed_tables: @@ -3741,7 +3891,7 @@ def clear_table_data(table_name: str, admin_user: Dict[str, Any] = Depends(requi 'cookies', 'cookie_status', 'keywords', 'default_replies', 'default_reply_records', 'ai_reply_settings', 'ai_conversations', 'ai_item_cache', 'item_info', 'message_notifications', 'cards', 'delivery_rules', 'notification_channels', - 'user_settings', 'system_settings', 'email_verifications', 'captcha_codes', 'orders' + 'user_settings', 'system_settings', 'email_verifications', 'captcha_codes', 'orders', "item_replay" ] # 不允许清空用户表 diff --git a/static/data_management.html b/static/data_management.html index 207168a..a931dcf 100644 --- a/static/data_management.html +++ b/static/data_management.html @@ -98,6 +98,7 @@ + @@ -263,6 +264,7 @@ 'cookies': 'Cookie账号表', 'cookie_status': 'Cookie状态表', 'keywords': '关键字表', + 'item_replay': '指定商品回复表', 'default_replies': '默认回复表', 'default_reply_records': '默认回复记录表', 'ai_reply_settings': 'AI回复设置表', diff --git a/static/index.html b/static/index.html index cdc5317..8670597 100644 --- a/static/index.html +++ b/static/index.html @@ -54,6 +54,12 @@ 自动回复 + + +
+
+

+ + 商品回复管理 +

+

管理各账号的商品信息

+
+ +
+ +
+
+
+
+ + +
+
+
+ +
+ + +
+
+ + +
+
+
+
+
+
+ + +
+
+
商品列表(自动发货根据商品标题和商品详情匹配关键字)
+ +
+
+
+ + + + + + + + + + + + + + + + + + +
+ + 账号ID商品ID商品标题商品内容回复内容更新时间操作
加载中...
+
+
+
+
+
+ + + + + +
diff --git a/static/js/app.js b/static/js/app.js index bf239df..546cad9 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -77,6 +77,9 @@ function showSection(sectionName) { case 'items': // 【商品管理菜单】 loadItems(); break; + case 'items-reply': // 【商品回复管理菜单】 + loadItemsReplay(); + break; case 'orders': // 【订单管理菜单】 loadOrders(); break; @@ -4877,7 +4880,7 @@ async function toggleItemMultiSpec(cookieId, itemId, isMultiSpec) { async function loadItems() { try { // 先加载Cookie列表用于筛选 - await loadCookieFilter(); + await loadCookieFilter('itemCookieFilter'); // 加载商品列表 await refreshItemsData(); @@ -4903,7 +4906,7 @@ async function refreshItemsData() { } // 加载Cookie筛选选项 -async function loadCookieFilter() { +async function loadCookieFilter(id) { try { const response = await fetch(`${apiBase}/cookies/details`, { headers: { @@ -4913,7 +4916,7 @@ async function loadCookieFilter() { if (response.ok) { const accounts = await response.json(); - const select = document.getElementById('itemCookieFilter'); + const select = document.getElementById(id); // 保存当前选择的值 const currentValue = select.value; @@ -5647,6 +5650,455 @@ function escapeHtml(text) { return div.innerHTML; } +// ================================ +// 【商品回复管理菜单】相关功能 +// ================================ + +// 加载商品回复列表 +async function loadItemsReplay() { + try { + // 先加载Cookie列表用于筛选 + await loadCookieFilter('itemReplayCookieFilter'); + await loadCookieFilterPlus('editReplyCookieIdSelect'); + // 加载商品列表 + await refreshItemsReplayData(); + } catch (error) { + console.error('加载商品列表失败:', error); + showToast('加载商品列表失败', 'danger'); + } +} + +// 只刷新商品回复数据,不重新加载筛选器 +async function refreshItemsReplayData() { + try { + const selectedCookie = document.getElementById('itemCookieFilter').value; + if (selectedCookie) { + await loadItemsReplayByCookie(); + } else { + await loadAllItemReplays(); + } + } catch (error) { + console.error('刷新商品数据失败:', error); + showToast('刷新商品数据失败', 'danger'); + } +} + +// 加载Cookie筛选选项添加弹框中使用 +async function loadCookieFilterPlus(id) { + try { + const response = await fetch(`${apiBase}/cookies/details`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + if (response.ok) { + const accounts = await response.json(); + const select = document.getElementById(id); + + // 保存当前选择的值 + const currentValue = select.value; + + // 清空现有选项(保留"所有账号") + select.innerHTML = ''; + + if (accounts.length === 0) { + const option = document.createElement('option'); + option.value = ''; + option.textContent = '❌ 暂无账号'; + option.disabled = true; + select.appendChild(option); + return; + } + + // 分组显示:先显示启用的账号,再显示禁用的账号 + const enabledAccounts = accounts.filter(account => { + const enabled = account.enabled === undefined ? true : account.enabled; + return enabled; + }); + const disabledAccounts = accounts.filter(account => { + const enabled = account.enabled === undefined ? true : account.enabled; + return !enabled; + }); + + // 添加启用的账号 + enabledAccounts.forEach(account => { + const option = document.createElement('option'); + option.value = account.id; + option.textContent = `🟢 ${account.id}`; + select.appendChild(option); + }); + + // 添加禁用的账号 + if (disabledAccounts.length > 0) { + // 添加分隔线 + if (enabledAccounts.length > 0) { + const separator = document.createElement('option'); + separator.value = ''; + separator.textContent = '────────────────'; + separator.disabled = true; + select.appendChild(separator); + } + + disabledAccounts.forEach(account => { + const option = document.createElement('option'); + option.value = account.id; + option.textContent = `🔴 ${account.id} (已禁用)`; + select.appendChild(option); + }); + } + + // 恢复之前选择的值 + if (currentValue) { + select.value = currentValue; + } + } + } catch (error) { + console.error('加载Cookie列表失败:', error); + showToast('加载账号列表失败', 'danger'); + } +} + +// 刷新商品回复列表 +async function refreshItemReplayS() { + await refreshItemsReplayData(); + showToast('商品列表已刷新', 'success'); +} + +// 加载所有商品回复 +async function loadAllItemReplays() { + try { + const response = await fetch(`${apiBase}/itemReplays`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + if (response.ok) { + const data = await response.json(); + displayItemReplays(data.items); + } else { + throw new Error('获取商品列表失败'); + } + } catch (error) { + console.error('加载商品列表失败:', error); + showToast('加载商品列表失败', 'danger'); + } +} + +// 按Cookie加载商品回复 +async function loadItemsReplayByCookie() { + const cookieId = document.getElementById('itemReplayCookieFilter').value; + if (!cookieId) { + await loadAllItemReplays(); + return; + } + + try { + const response = await fetch(`${apiBase}/itemReplays/cookie/${encodeURIComponent(cookieId)}`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + if (response.ok) { + const data = await response.json(); + displayItemReplays(data.items); + } else { + throw new Error('获取商品列表失败'); + } + } catch (error) { + console.error('加载商品列表失败:', error); + showToast('加载商品列表失败', 'danger'); + } +} + +// 显示商品回复列表 +function displayItemReplays(items) { + const tbody = document.getElementById('itemReplaysTableBody'); + + if (!items || items.length === 0) { + tbody.innerHTML = '暂无商品数据'; + // 重置选择状态 + const selectAllCheckbox = document.getElementById('selectAllItems'); + if (selectAllCheckbox) { + selectAllCheckbox.checked = false; + selectAllCheckbox.indeterminate = false; + } + updateBatchDeleteButton(); + return; + } + + const itemsHtml = items.map(item => { + // 处理商品标题显示 + let itemTitleDisplay = item.item_title || '未设置'; + if (itemTitleDisplay.length > 30) { + itemTitleDisplay = itemTitleDisplay.substring(0, 30) + '...'; + } + + // 处理商品详情显示 + let itemDetailDisplay = '未设置'; + if (item.item_detail) { + try { + // 尝试解析JSON并提取有用信息 + const detail = JSON.parse(item.item_detail); + if (detail.content) { + itemDetailDisplay = detail.content.substring(0, 50) + (detail.content.length > 50 ? '...' : ''); + } else { + // 如果是纯文本或其他格式,直接显示前50个字符 + itemDetailDisplay = item.item_detail.substring(0, 50) + (item.item_detail.length > 50 ? '...' : ''); + } + } catch (e) { + // 如果不是JSON格式,直接显示前50个字符 + itemDetailDisplay = item.item_detail.substring(0, 50) + (item.item_detail.length > 50 ? '...' : ''); + } + } + + return ` + + + + + ${escapeHtml(item.cookie_id)} + ${escapeHtml(item.item_id)} + ${escapeHtml(itemTitleDisplay)} + ${escapeHtml(itemDetailDisplay)} + ${escapeHtml(item.reply_content)} + ${formatDateTime(item.updated_at)} + +
+ + +
+ + + `; + }).join(''); + + // 更新表格内容 + tbody.innerHTML = itemsHtml; + + // 重置选择状态 + const selectAllCheckbox = document.getElementById('selectAllItems'); + if (selectAllCheckbox) { + selectAllCheckbox.checked = false; + selectAllCheckbox.indeterminate = false; + } + updateBatchDeleteButton(); +} + +// 显示添加弹框 +async function showItemReplayEdit(){ + // 显示模态框 + const modal = new bootstrap.Modal(document.getElementById('editItemReplyModal')); + document.getElementById('editReplyCookieIdSelect').value = ''; + document.getElementById('editReplyItemIdSelect').value = ''; + document.getElementById('editReplyItemIdSelect').disabled = true + document.getElementById('editItemReplyContent').value = ''; + document.getElementById('itemReplayTitle').textContent = '添加商品回复'; + modal.show(); +} + +// 当账号变化时加载对应商品 +async function onCookieChangeForReply() { + const cookieId = document.getElementById('editReplyCookieIdSelect').value; + const itemSelect = document.getElementById('editReplyItemIdSelect'); + + itemSelect.innerHTML = ''; + if (!cookieId) { + itemSelect.disabled = true; // 禁用选择框 + return; + } else { + itemSelect.disabled = false; // 启用选择框 + } + + const response = await fetch(`${apiBase}/items/cookie/${encodeURIComponent(cookieId)}`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + try { + if (response.ok) { + const data = await response.json(); + data.items.forEach(item => { + const opt = document.createElement('option'); + opt.value = item.item_id; + opt.textContent = `${item.item_id} - ${item.item_title || '无标题'}`; + itemSelect.appendChild(opt); + }); + } else { + throw new Error('获取商品列表失败'); + } + }catch (error) { + console.error('加载商品列表失败:', error); + showToast('加载商品列表失败', 'danger'); + } +} + +// 编辑商品回复 +async function editItemReply(cookieId, itemId) { + try { + const response = await fetch(`${apiBase}/item-reply/${encodeURIComponent(cookieId)}/${encodeURIComponent(itemId)}`, { + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + if (response.ok) { + const data = await response.json(); + document.getElementById('itemReplayTitle').textContent = '编辑商品回复'; + // 填充表单 + document.getElementById('editReplyCookieIdSelect').value = data.cookie_id; + let res = await onCookieChangeForReply() + document.getElementById('editReplyItemIdSelect').value = data.item_id; + document.getElementById('editItemReplyContent').value = data.reply_content || ''; + + } else if (response.status === 404) { + // 如果没有记录,则填充空白内容(用于添加) +// document.getElementById('editReplyCookieIdSelect').value = data.cookie_id; +// document.getElementById('editReplyItemIdSelect').value = data.item_id; +// document.getElementById('editItemReplyContent').value = data.reply_content || ''; + } else { + throw new Error('获取商品回复失败'); + } + + // 显示模态框 + const modal = new bootstrap.Modal(document.getElementById('editItemReplyModal')); + modal.show(); + + } catch (error) { + console.error('获取商品回复失败:', error); + showToast('获取商品回复失败', 'danger'); + } +} + +// 保存商品回复 +async function saveItemReply() { + const cookieId = document.getElementById('editReplyCookieIdSelect').value; + const itemId = document.getElementById('editReplyItemIdSelect').value; + const replyContent = document.getElementById('editItemReplyContent').value.trim(); + + console.log(cookieId) + console.log(itemId) + console.log(replyContent) + if (!cookieId) { + showToast('请选择账号', 'warning'); + return; + } + + if (!itemId) { + showToast('请选择商品', 'warning'); + return; + } + + if (!replyContent) { + showToast('请输入商品回复内容', 'warning'); + return; + } + + try { + const response = await fetch(`${apiBase}/item-reply/${encodeURIComponent(cookieId)}/${encodeURIComponent(itemId)}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + body: JSON.stringify({ + reply_content: replyContent + }) + }); + + if (response.ok) { + showToast('商品回复保存成功', 'success'); + + // 关闭模态框 + const modal = bootstrap.Modal.getInstance(document.getElementById('editItemReplyModal')); + modal.hide(); + + // 可选:刷新数据 + await refreshItemsReplayData?.(); + } else { + const error = await response.text(); + showToast(`保存失败: ${error}`, 'danger'); + } + } catch (error) { + console.error('保存商品回复失败:', error); + showToast('保存商品回复失败', 'danger'); + } +} + +// 删除商品回复 +async function deleteItemReply(cookieId, itemId, itemTitle) { + try { + const confirmed = confirm(`确定要删除该商品的自动回复吗?\n\n商品ID: ${itemId}\n商品标题: ${itemTitle || '未设置'}\n\n此操作不可撤销!`); + if (!confirmed) return; + + const response = await fetch(`${apiBase}/item-reply/${encodeURIComponent(cookieId)}/${encodeURIComponent(itemId)}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${authToken}` + } + }); + + if (response.ok) { + showToast('商品回复删除成功', 'success'); + await loadItemsReplayByCookie?.(); // 如果你有刷新商品列表的函数 + } else { + const error = await response.text(); + showToast(`删除失败: ${error}`, 'danger'); + } + } catch (error) { + console.error('删除商品回复失败:', error); + showToast('删除商品回复失败', 'danger'); + } +} + +// 批量删除商品回复 +async function batchDeleteItemReplies() { + try { + const checkboxes = document.querySelectorAll('input[name="itemCheckbox"]:checked'); + if (checkboxes.length === 0) { + showToast('请选择要删除回复的商品', 'warning'); + return; + } + + const confirmed = confirm(`确定要删除选中商品的自动回复吗?\n共 ${checkboxes.length} 个商品\n\n此操作不可撤销!`); + if (!confirmed) return; + + const itemsToDelete = Array.from(checkboxes).map(checkbox => ({ + cookie_id: checkbox.dataset.cookieId, + item_id: checkbox.dataset.itemId + })); + + const response = await fetch(`${apiBase}/item-reply/batch`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + body: JSON.stringify({ items: itemsToDelete }) + }); + + if (response.ok) { + const result = await response.json(); + showToast(`批量删除回复完成: 成功 ${result.success_count} 个,失败 ${result.failed_count} 个`, 'success'); + await loadItemsReplayByCookie?.(); + } else { + const error = await response.text(); + showToast(`批量删除失败: ${error}`, 'danger'); + } + } catch (error) { + console.error('批量删除商品回复失败:', error); + showToast('批量删除商品回复失败', 'danger'); + } +} + // ================================ // 【日志管理菜单】相关功能 // ================================