mirror of
https://github.com/zhinianboke/xianyu-auto-reply.git
synced 2025-08-30 01:27:35 +08:00
支持指定商品默认回复
This commit is contained in:
parent
46f7066519
commit
5b2a991f41
@ -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}】默认回复内容为空,跳过自动回复")
|
||||
|
171
check_user_credentials.py
Normal file
171
check_user_credentials.py
Normal file
@ -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请使用这些凭据登录管理后台测试账号下拉框功能")
|
192
check_user_table.py
Normal file
192
check_user_table.py
Normal file
@ -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. 如果仍然没有数据,请检查浏览器开发者工具")
|
204
db_manager.py
204
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}
|
||||
|
||||
|
||||
|
||||
# 全局单例
|
||||
|
154
reply_server.py
154
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"
|
||||
]
|
||||
|
||||
# 不允许清空用户表
|
||||
|
@ -98,6 +98,7 @@
|
||||
<option value="cookie_status">cookie_status - Cookie状态表</option>
|
||||
<option value="keywords">keywords - 关键字表</option>
|
||||
<option value="default_replies">default_replies - 默认回复表</option>
|
||||
<option value="item_replay">item_replay - 指定商品回复表</option>
|
||||
<option value="default_reply_records">default_reply_records - 默认回复记录表</option>
|
||||
<option value="ai_reply_settings">ai_reply_settings - AI回复设置表</option>
|
||||
<option value="ai_conversations">ai_conversations - AI对话历史表</option>
|
||||
@ -263,6 +264,7 @@
|
||||
'cookies': 'Cookie账号表',
|
||||
'cookie_status': 'Cookie状态表',
|
||||
'keywords': '关键字表',
|
||||
'item_replay': '指定商品回复表',
|
||||
'default_replies': '默认回复表',
|
||||
'default_reply_records': '默认回复记录表',
|
||||
'ai_reply_settings': 'AI回复设置表',
|
||||
|
@ -54,6 +54,12 @@
|
||||
自动回复
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a href="#" class="nav-link" onclick="showSection('items-reply')">
|
||||
<i class="bi bi-chat-left-text"></i>
|
||||
指定商品回复
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a href="#" class="nav-link" onclick="showSection('cards')">
|
||||
<i class="bi bi-credit-card"></i>
|
||||
@ -397,6 +403,7 @@
|
||||
<th style="width: 12%">商品ID</th>
|
||||
<th style="width: 18%">商品标题</th>
|
||||
<th style="width: 20%">商品详情</th>
|
||||
<th style="width: 20%">商品价格</th>
|
||||
<th style="width: 8%">多规格</th>
|
||||
<th style="width: 10%">更新时间</th>
|
||||
<th style="width: 15%">操作</th>
|
||||
@ -456,6 +463,140 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 指定商品自动管理内容 -->
|
||||
<div id="items-reply-section" class="content-section">
|
||||
<div class="content-header">
|
||||
<h2 class="mb-0">
|
||||
<i class="bi bi-box-seam me-2"></i>
|
||||
商品回复管理
|
||||
</h2>
|
||||
<p class="text-muted mb-0">管理各账号的商品信息</p>
|
||||
</div>
|
||||
|
||||
<div class="content-body">
|
||||
<!-- Cookie筛选 -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-end">
|
||||
<div class="col-md-6">
|
||||
<label for="itemCookieFilter" class="form-label">筛选账号</label>
|
||||
<select class="form-select" id="itemReplayCookieFilter" onchange="loadItemsReplayByCookie()">
|
||||
<option value="">所有账号</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex justify-content-end align-items-end gap-2">
|
||||
<!-- 页码输入 -->
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<label for="pageNumber" class="form-label mb-0 text-nowrap">页码:</label>
|
||||
<input type="number" class="form-control" id="pageNumber" placeholder="页码" min="1" value="1" style="width: 80px;">
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-success" onclick="showItemReplayEdit()">
|
||||
添加商品回复
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="refreshItemReplayS()">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">商品列表(自动发货根据商品标题和商品详情匹配关键字)</h5>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="batchDeleteItemReplies()">
|
||||
<i class="bi bi-trash"></i> 批量删除
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 5%">
|
||||
<input type="checkbox" id="selectAllItemReplay" onchange="toggleSelectAll(this)">
|
||||
</th>
|
||||
<th style="width: 12%">账号ID</th>
|
||||
<th style="width: 12%">商品ID</th>
|
||||
<th style="width: 18%">商品标题</th>
|
||||
<th style="width: 20%">商品内容</th>
|
||||
<th style="width: 18%">回复内容</th>
|
||||
<th style="width: 10%">更新时间</th>
|
||||
<th style="width: 15%">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="itemReplaysTableBody">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted">加载中...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品回复弹框 -->
|
||||
<!-- 添加/编辑商品回复模态框 -->
|
||||
<div class="modal fade" id="editItemReplyModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-chat-text me-2"></i><span id="itemReplayTitle">编辑商品回复</span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editItemReplyForm">
|
||||
<input type="hidden" id="editReplyCookieId">
|
||||
<input type="hidden" id="editReplyItemId">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">账号ID<span class="text-danger">*</span></label>
|
||||
<select id="editReplyCookieIdSelect" class="form-select" onchange="onCookieChangeForReply()">
|
||||
<option value="">请选择账号</option>
|
||||
<!-- JS 动态填充账号选项 -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">商品ID<span class="text-danger">*</span></label>
|
||||
<select id="editReplyItemIdSelect" class="form-select">
|
||||
<option value="">选择商品</option>
|
||||
<!-- JS 动态填充商品选项 -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="editReplyContent" class="form-label">商品回复内容 <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="editItemReplyContent" rows="10"
|
||||
placeholder="请输入商品回复内容..."></textarea>
|
||||
<div class="form-text">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
请输入商品的自动回复内容,用户购买后将收到该回复。
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveItemReply()">
|
||||
<i class="bi bi-check-circle me-1"></i>保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 订单管理内容 -->
|
||||
<div id="orders-section" class="content-section">
|
||||
<div class="content-header">
|
||||
|
458
static/js/app.js
458
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 = '<option value="">选择账号</option>';
|
||||
|
||||
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 = '<tr><td colspan="5" class="text-center text-muted">暂无商品数据</td></tr>';
|
||||
// 重置选择状态
|
||||
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 `
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="itemCheckbox"
|
||||
data-cookie-id="${escapeHtml(item.cookie_id)}"
|
||||
data-item-id="${escapeHtml(item.item_id)}"
|
||||
onchange="updateSelectAllState()">
|
||||
</td>
|
||||
<td>${escapeHtml(item.cookie_id)}</td>
|
||||
<td>${escapeHtml(item.item_id)}</td>
|
||||
<td title="${escapeHtml(item.item_title || '未设置')}">${escapeHtml(itemTitleDisplay)}</td>
|
||||
<td title="${escapeHtml(item.item_detail || '未设置')}">${escapeHtml(itemDetailDisplay)}</td>
|
||||
<td title="${escapeHtml(item.reply_content || '未设置')}">${escapeHtml(item.reply_content)}</td>
|
||||
<td>${formatDateTime(item.updated_at)}</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="editItemReply('${escapeHtml(item.cookie_id)}', '${escapeHtml(item.item_id)}')" title="编辑详情">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteItemReply('${escapeHtml(item.cookie_id)}', '${escapeHtml(item.item_id)}', '${escapeHtml(item.item_title || item.item_id)}')" title="删除">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).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 = '<option value="">选择商品</option>';
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
// ================================
|
||||
// 【日志管理菜单】相关功能
|
||||
// ================================
|
||||
|
Loading…
x
Reference in New Issue
Block a user