支持指定商品默认回复

This commit is contained in:
zhinianboke 2025-08-07 22:50:02 +08:00
parent 46f7066519
commit 5b2a991f41
8 changed files with 1345 additions and 9 deletions

View File

@ -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
View 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
View 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. 如果仍然没有数据,请检查浏览器开发者工具")

View File

@ -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}
# 全局单例

View File

@ -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"
]
# 不允许清空用户表

View File

@ -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回复设置表',

View File

@ -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">

View File

@ -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');
}
}
// ================================
// 【日志管理菜单】相关功能
// ================================