Compare commits

...

2 Commits

Author SHA1 Message Date
zhinianboke
aae9d1ab46 自动回复增加人工接入后等待时间 2025-08-05 16:11:27 +08:00
zhinianboke
d53a114dba 添加是否开启注册开关 2025-08-05 15:03:38 +08:00
8 changed files with 970 additions and 21 deletions

121
AUTO_REPLY_PAUSE_README.md Normal file
View File

@ -0,0 +1,121 @@
# 自动回复暂停功能说明
## 功能概述
当系统检测到某个 `chat_id` 有手动发出的消息时,会自动暂停该 `chat_id` 的自动回复功能10分钟。如果在暂停期间再次检测到手动发出的消息会重新开始计时10分钟。
## 功能特性
### 1. 智能检测
- 系统会自动检测每个聊天会话中的手动发出消息
- 检测到手动发出时会在日志中显示:`[时间] 【手动发出】 商品(商品ID): 消息内容`
### 2. 自动暂停
- 检测到手动发出后,立即暂停该 `chat_id` 的自动回复10分钟
- 暂停期间会在日志中显示:`【账号ID】检测到手动发出消息chat_id XXX 自动回复暂停10分钟恢复时间: YYYY-MM-DD HH:MM:SS`
### 3. 重新计时
- 如果在暂停期间再次检测到手动发出会重新开始计时10分钟
- 每次手动发出都会刷新暂停时间
### 4. 暂停提示
- 当收到消息但处于暂停状态时,会在日志中显示:
`【账号ID】【系统】chat_id XXX 自动回复已暂停,剩余时间: X分Y秒`
### 5. 自动恢复
- 暂停时间到期后,自动恢复该 `chat_id` 的自动回复功能
- 无需手动干预
## 技术实现
### 核心组件
#### AutoReplyPauseManager 类
- `pause_chat(chat_id, cookie_id)`: 暂停指定chat_id的自动回复
- `is_chat_paused(chat_id)`: 检查指定chat_id是否处于暂停状态
- `get_remaining_pause_time(chat_id)`: 获取剩余暂停时间
- `cleanup_expired_pauses()`: 清理过期的暂停记录
#### 集成点
1. **检测手动发出** (第2730行)
```python
if send_user_id == self.myid:
logger.info(f"[{msg_time}] 【手动发出】 商品({item_id}): {send_message}")
# 暂停该chat_id的自动回复10分钟
pause_manager.pause_chat(chat_id, self.cookie_id)
return
```
2. **检查暂停状态** (第2750行)
```python
# 检查该chat_id是否处于暂停状态
if pause_manager.is_chat_paused(chat_id):
remaining_time = pause_manager.get_remaining_pause_time(chat_id)
remaining_minutes = remaining_time // 60
remaining_seconds = remaining_time % 60
logger.info(f"[{msg_time}] 【{self.cookie_id}】【系统】chat_id {chat_id} 自动回复已暂停,剩余时间: {remaining_minutes}分{remaining_seconds}秒")
return
```
3. **定期清理** (第2372行)
```python
async def pause_cleanup_loop(self):
"""定期清理过期的暂停记录"""
while True:
# 每5分钟清理一次过期记录
pause_manager.cleanup_expired_pauses()
await asyncio.sleep(300)
```
## 配置参数
### 暂停时长
- 默认10分钟 (600秒)
- 位置:`AutoReplyPauseManager.__init__()` 中的 `self.pause_duration`
- 可根据需要修改
### 清理频率
- 默认每5分钟清理一次过期记录
- 位置:`pause_cleanup_loop()` 中的 `await asyncio.sleep(300)`
## 日志示例
### 检测到手动发出
```
2025-08-05 14:48:32.209 | INFO | XianyuAutoAsync:handle_message:2673 - [2025-08-05 14:48:31] 【手动发出】 商品(12345): 你好,这个商品还在吗?
2025-08-05 14:48:32.210 | INFO | XianyuAutoAsync:pause_chat:40 - 【dfg】检测到手动发出消息chat_id chat_123 自动回复暂停10分钟恢复时间: 2025-08-05 14:58:32
```
### 暂停期间收到消息
```
2025-08-05 14:50:15.123 | INFO | XianyuAutoAsync:handle_message:2678 - [2025-08-05 14:50:15] 【收到】用户: 张三 (ID: 67890), 商品(12345): 多少钱?
2025-08-05 14:50:15.124 | INFO | XianyuAutoAsync:handle_message:2754 - [2025-08-05 14:50:15] 【dfg】【系统】chat_id chat_123 自动回复已暂停,剩余时间: 8分17秒
```
### 重新计时
```
2025-08-05 14:55:20.456 | INFO | XianyuAutoAsync:handle_message:2673 - [2025-08-05 14:55:20] 【手动发出】 商品(12345): 价格可以商量
2025-08-05 14:55:20.457 | INFO | XianyuAutoAsync:pause_chat:40 - 【dfg】检测到手动发出消息chat_id chat_123 自动回复暂停10分钟恢复时间: 2025-08-05 15:05:20
```
## 注意事项
1. **全局管理器**: 使用全局的 `pause_manager` 实例,所有账号共享暂停状态
2. **内存存储**: 暂停记录存储在内存中,程序重启后会丢失
3. **自动清理**: 系统会定期清理过期的暂停记录,避免内存泄漏
4. **线程安全**: 暂停管理器是线程安全的,可以在多个协程中使用
## 测试
运行测试脚本验证功能:
```bash
python test_pause_manager.py
```
测试包括:
- 基本暂停/恢复功能
- 重新计时机制
- 多chat_id管理
- 过期清理功能
- 时间计算准确性

185
PAUSE_DURATION_FEATURE.md Normal file
View File

@ -0,0 +1,185 @@
# 账号自动回复暂停时间配置功能
## 功能概述
为每个账号单独配置自动回复暂停时间,当检测到手动发出消息后,该账号的自动回复会暂停指定的时间长度。
## 功能特性
### 1. 个性化配置
- 每个账号可以单独设置暂停时间1-60分钟
- 默认暂停时间为10分钟
- 支持实时修改,立即生效
### 2. 直观界面
- 在账号管理表格中新增"暂停时间"列
- 点击暂停时间可直接编辑
- 带有说明工具提示,解释功能作用
### 3. 智能验证
- 暂停时间范围限制1-60分钟
- 输入验证和错误提示
- 支持键盘操作Enter保存Escape取消
## 界面展示
### 表格列头
```
账号ID | Cookie值 | 关键词 | 状态 | 默认回复 | AI回复 | 自动确认发货 | 备注 | 暂停时间 | 操作
```
### 暂停时间列
- 显示格式:`🕐 10分钟`
- 工具提示:`检测到手动发出消息后,自动回复暂停的时间长度(分钟)。如果在暂停期间再次手动发出消息,会重新开始计时。`
- 点击可编辑,支持数字输入框
## 技术实现
### 数据库结构
```sql
-- cookies表新增字段
ALTER TABLE cookies ADD COLUMN pause_duration INTEGER DEFAULT 10;
```
### 后端API
#### 1. 更新暂停时间
```http
PUT /cookies/{cid}/pause-duration
Content-Type: application/json
Authorization: Bearer {token}
{
"pause_duration": 15
}
```
**响应**
```json
{
"message": "暂停时间更新成功",
"pause_duration": 15
}
```
#### 2. 获取暂停时间
```http
GET /cookies/{cid}/pause-duration
Authorization: Bearer {token}
```
**响应**
```json
{
"pause_duration": 15,
"message": "获取暂停时间成功"
}
```
### 前端功能
#### 1. 表格显示
- 在账号列表中显示每个账号的暂停时间
- 默认显示为"🕐 10分钟"格式
#### 2. 内联编辑
```javascript
function editPauseDuration(cookieId, currentDuration) {
// 创建数字输入框
// 支持1-60分钟范围
// Enter保存Escape取消
// 实时验证和错误提示
}
```
#### 3. 工具提示
- 使用Bootstrap Tooltip组件
- 自动初始化和重新初始化
### 暂停管理器集成
#### 动态获取暂停时间
```python
def pause_chat(self, chat_id: str, cookie_id: str):
"""暂停指定chat_id的自动回复使用账号特定的暂停时间"""
# 获取账号特定的暂停时间
try:
from db_manager import db_manager
pause_minutes = db_manager.get_cookie_pause_duration(cookie_id)
except Exception as e:
logger.error(f"获取账号 {cookie_id} 暂停时间失败: {e}使用默认10分钟")
pause_minutes = 10
pause_duration_seconds = pause_minutes * 60
pause_until = time.time() + pause_duration_seconds
self.paused_chats[chat_id] = pause_until
# 记录日志
end_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(pause_until))
logger.info(f"【{cookie_id}】检测到手动发出消息chat_id {chat_id} 自动回复暂停{pause_minutes}分钟,恢复时间: {end_time}")
```
## 使用流程
### 1. 查看当前设置
1. 登录系统,进入账号管理页面
2. 在表格中查看"暂停时间"列
3. 鼠标悬停在列头的问号图标上查看功能说明
### 2. 修改暂停时间
1. 点击要修改的账号的暂停时间(如"🕐 10分钟"
2. 输入框出现输入新的暂停时间1-60分钟
3. 按Enter键保存或按Escape键取消
4. 系统显示成功提示
### 3. 验证生效
1. 修改后的设置立即生效
2. 下次该账号检测到手动发出消息时,会使用新的暂停时间
3. 在日志中可以看到使用的暂停时间
## 日志示例
### 使用自定义暂停时间
```
2025-08-05 15:30:15.123 | INFO | XianyuAutoAsync:pause_chat:49 - 【abc123】检测到手动发出消息chat_id chat_456 自动回复暂停15分钟恢复时间: 2025-08-05 15:45:15
```
### 获取暂停时间失败时使用默认值
```
2025-08-05 15:30:15.124 | ERROR | XianyuAutoAsync:pause_chat:42 - 获取账号 abc123 暂停时间失败: Database error使用默认10分钟
2025-08-05 15:30:15.125 | INFO | XianyuAutoAsync:pause_chat:49 - 【abc123】检测到手动发出消息chat_id chat_456 自动回复暂停10分钟恢复时间: 2025-08-05 15:40:15
```
## 配置建议
### 不同场景的推荐设置
1. **高频互动商品**5-10分钟
- 适用于需要快速响应的热门商品
- 减少客户等待时间
2. **普通商品**10-15分钟默认
- 平衡自动化和人工干预
- 适合大多数场景
3. **低频互动商品**20-30分钟
- 适用于不常有咨询的商品
- 给予更多人工处理时间
4. **特殊商品**30-60分钟
- 需要详细沟通的复杂商品
- 避免自动回复干扰深度交流
## 注意事项
1. **范围限制**暂停时间必须在1-60分钟之间
2. **立即生效**:修改后的设置立即生效,无需重启
3. **默认值**新账号默认使用10分钟暂停时间
4. **错误处理**获取暂停时间失败时自动使用默认10分钟
5. **权限控制**:只能修改自己账号的暂停时间设置
## 兼容性
- **向后兼容**现有账号自动获得默认10分钟设置
- **数据迁移**系统启动时自动添加pause_duration字段
- **API兼容**现有API继续正常工作新增字段可选

View File

@ -21,6 +21,70 @@ from utils.ws_utils import WebSocketClient
import sys
import aiohttp
class AutoReplyPauseManager:
"""自动回复暂停管理器"""
def __init__(self):
# 存储每个chat_id的暂停信息 {chat_id: pause_until_timestamp}
self.paused_chats = {}
def pause_chat(self, chat_id: str, cookie_id: str):
"""暂停指定chat_id的自动回复使用账号特定的暂停时间"""
# 获取账号特定的暂停时间
try:
from db_manager import db_manager
pause_minutes = db_manager.get_cookie_pause_duration(cookie_id)
except Exception as e:
logger.error(f"获取账号 {cookie_id} 暂停时间失败: {e}使用默认10分钟")
pause_minutes = 10
pause_duration_seconds = pause_minutes * 60
pause_until = time.time() + pause_duration_seconds
self.paused_chats[chat_id] = pause_until
# 计算暂停结束时间
end_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(pause_until))
logger.info(f"{cookie_id}】检测到手动发出消息chat_id {chat_id} 自动回复暂停{pause_minutes}分钟,恢复时间: {end_time}")
def is_chat_paused(self, chat_id: str) -> bool:
"""检查指定chat_id是否处于暂停状态"""
if chat_id not in self.paused_chats:
return False
current_time = time.time()
pause_until = self.paused_chats[chat_id]
if current_time >= pause_until:
# 暂停时间已过,移除记录
del self.paused_chats[chat_id]
return False
return True
def get_remaining_pause_time(self, chat_id: str) -> int:
"""获取指定chat_id剩余暂停时间"""
if chat_id not in self.paused_chats:
return 0
current_time = time.time()
pause_until = self.paused_chats[chat_id]
remaining = max(0, int(pause_until - current_time))
return remaining
def cleanup_expired_pauses(self):
"""清理已过期的暂停记录"""
current_time = time.time()
expired_chats = [chat_id for chat_id, pause_until in self.paused_chats.items()
if current_time >= pause_until]
for chat_id in expired_chats:
del self.paused_chats[chat_id]
# 全局暂停管理器实例
pause_manager = AutoReplyPauseManager()
# 日志配置
log_dir = 'logs'
os.makedirs(log_dir, exist_ok=True)
@ -106,10 +170,13 @@ class XianyuLive:
# 自动确认发货防重复机制
self.confirmed_orders = {} # 记录已确认发货的订单,防止重复确认
self.order_confirm_cooldown = 600 # 10分钟内不重复确认同一订单
self.session = None # 用于API调用的aiohttp session
# 启动定期清理过期暂停记录的任务
self.cleanup_task = None
def is_auto_confirm_enabled(self) -> bool:
"""检查当前账号是否启用自动确认发货"""
try:
@ -2301,6 +2368,25 @@ class XianyuLive:
logger.error(f"处理心跳响应出错: {self._safe_str(e)}")
return False
async def pause_cleanup_loop(self):
"""定期清理过期的暂停记录"""
while True:
try:
# 检查账号是否启用
from cookie_manager import manager as cookie_manager
if cookie_manager and not cookie_manager.get_cookie_status(self.cookie_id):
logger.info(f"{self.cookie_id}】账号已禁用,停止暂停记录清理循环")
break
# 清理过期的暂停记录
pause_manager.cleanup_expired_pauses()
# 每5分钟清理一次
await asyncio.sleep(300)
except Exception as e:
logger.error(f"{self.cookie_id}】暂停记录清理失败: {self._safe_str(e)}")
await asyncio.sleep(300) # 出错后也等待5分钟再重试
async def send_msg_once(self, toid, item_id, text):
headers = {
"Cookie": self.cookies_str,
@ -2672,6 +2758,8 @@ class XianyuLive:
if send_user_id == self.myid:
logger.info(f"[{msg_time}] 【手动发出】 商品({item_id}): {send_message}")
# 暂停该chat_id的自动回复10分钟
pause_manager.pause_chat(chat_id, self.cookie_id)
return
else:
@ -2686,6 +2774,14 @@ class XianyuLive:
logger.info(f"[{msg_time}] 【{self.cookie_id}】【系统】自动回复已禁用")
return
# 检查该chat_id是否处于暂停状态
if pause_manager.is_chat_paused(chat_id):
remaining_time = pause_manager.get_remaining_pause_time(chat_id)
remaining_minutes = remaining_time // 60
remaining_seconds = remaining_time % 60
logger.info(f"[{msg_time}] 【{self.cookie_id}】【系统】chat_id {chat_id} 自动回复已暂停,剩余时间: {remaining_minutes}{remaining_seconds}")
return
# 构造用户URL
user_url = f'https://www.goofish.com/personal?userId={send_user_id}'
@ -2878,6 +2974,11 @@ class XianyuLive:
logger.info(f"{self.cookie_id}】启动token刷新任务...")
self.token_refresh_task = asyncio.create_task(self.token_refresh_loop())
# 启动暂停记录清理任务
if not self.cleanup_task:
logger.info(f"{self.cookie_id}】启动暂停记录清理任务...")
self.cleanup_task = asyncio.create_task(self.pause_cleanup_loop())
logger.info(f"{self.cookie_id}】开始监听WebSocket消息...")
async for message in websocket:
@ -2901,9 +3002,18 @@ class XianyuLive:
self.heartbeat_task.cancel()
if self.token_refresh_task:
self.token_refresh_task.cancel()
if self.cleanup_task:
self.cleanup_task.cancel()
await asyncio.sleep(5) # 等待5秒后重试
continue
finally:
# 清理所有任务
if self.heartbeat_task:
self.heartbeat_task.cancel()
if self.token_refresh_task:
self.token_refresh_task.cancel()
if self.cleanup_task:
self.cleanup_task.cancel()
await self.close_session() # 确保关闭session
async def get_item_list_info(self, page_number=1, page_size=20, retry_count=0):

View File

@ -114,6 +114,7 @@ class DBManager:
user_id INTEGER NOT NULL,
auto_confirm INTEGER DEFAULT 1,
remark TEXT DEFAULT '',
pause_duration INTEGER DEFAULT 10,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
@ -342,7 +343,8 @@ class DBManager:
# 插入默认系统设置不包括管理员密码由reply_server.py初始化
cursor.execute('''
INSERT OR IGNORE INTO system_settings (key, value, description) VALUES
('theme_color', 'blue', '主题颜色')
('theme_color', 'blue', '主题颜色'),
('registration_enabled', 'true', '是否开启用户注册')
''')
# 检查并升级数据库
@ -382,6 +384,12 @@ class DBManager:
cursor.execute("ALTER TABLE cookies ADD COLUMN remark TEXT DEFAULT ''")
logger.info("数据库迁移完成添加remark列")
# 检查cookies表是否存在pause_duration列
if 'pause_duration' not in cookie_columns:
logger.info("添加cookies表的pause_duration列...")
cursor.execute("ALTER TABLE cookies ADD COLUMN pause_duration INTEGER DEFAULT 10")
logger.info("数据库迁移完成添加pause_duration列")
except Exception as e:
logger.error(f"数据库迁移失败: {e}")
# 迁移失败不应该阻止程序启动
@ -1087,11 +1095,11 @@ class DBManager:
return None
def get_cookie_details(self, cookie_id: str) -> Optional[Dict[str, any]]:
"""获取Cookie的详细信息包括user_id、auto_confirm和remark"""
"""获取Cookie的详细信息包括user_id、auto_confirm、remark和pause_duration"""
with self.lock:
try:
cursor = self.conn.cursor()
self._execute_sql(cursor, "SELECT id, value, user_id, auto_confirm, remark, created_at FROM cookies WHERE id = ?", (cookie_id,))
self._execute_sql(cursor, "SELECT id, value, user_id, auto_confirm, remark, pause_duration, created_at FROM cookies WHERE id = ?", (cookie_id,))
result = cursor.fetchone()
if result:
return {
@ -1100,7 +1108,8 @@ class DBManager:
'user_id': result[2],
'auto_confirm': bool(result[3]),
'remark': result[4] or '',
'created_at': result[5]
'pause_duration': result[5] or 10,
'created_at': result[6]
}
return None
except Exception as e:
@ -1133,6 +1142,33 @@ class DBManager:
logger.error(f"更新账号备注失败: {e}")
return False
def update_cookie_pause_duration(self, cookie_id: str, pause_duration: int) -> bool:
"""更新Cookie的自动回复暂停时间"""
with self.lock:
try:
cursor = self.conn.cursor()
self._execute_sql(cursor, "UPDATE cookies SET pause_duration = ? WHERE id = ?", (pause_duration, cookie_id))
self.conn.commit()
logger.info(f"更新账号 {cookie_id} 自动回复暂停时间: {pause_duration}分钟")
return True
except Exception as e:
logger.error(f"更新账号自动回复暂停时间失败: {e}")
return False
def get_cookie_pause_duration(self, cookie_id: str) -> int:
"""获取Cookie的自动回复暂停时间"""
with self.lock:
try:
cursor = self.conn.cursor()
self._execute_sql(cursor, "SELECT pause_duration FROM cookies WHERE id = ?", (cookie_id,))
result = cursor.fetchone()
if result:
return result[0] or 10 # 默认10分钟
return 10 # 如果没有找到记录,返回默认值
except Exception as e:
logger.error(f"获取账号自动回复暂停时间失败: {e}")
return 10 # 出错时返回默认值
def get_auto_confirm(self, cookie_id: str) -> bool:
"""获取Cookie的自动确认发货设置"""
with self.lock:

View File

@ -85,6 +85,8 @@ class LoginResponse(BaseModel):
token: Optional[str] = None
message: str
user_id: Optional[int] = None
username: Optional[str] = None
is_admin: Optional[bool] = None
class ChangePasswordRequest(BaseModel):
@ -397,6 +399,33 @@ async def login_page():
# 注册页面路由
@app.get('/register.html', response_class=HTMLResponse)
async def register_page():
# 检查注册是否开启
from db_manager import db_manager
registration_enabled = db_manager.get_system_setting('registration_enabled')
if registration_enabled != 'true':
return HTMLResponse('''
<!DOCTYPE html>
<html>
<head>
<title>注册已关闭</title>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
.message { color: #666; font-size: 18px; }
.back-link { margin-top: 20px; }
.back-link a { color: #007bff; text-decoration: none; }
</style>
</head>
<body>
<h2>🚫 注册功能已关闭</h2>
<p class="message">系统管理员已关闭用户注册功能</p>
<div class="back-link">
<a href="/"> 返回首页</a>
</div>
</body>
</html>
''', status_code=403)
register_path = os.path.join(static_dir, 'register.html')
if os.path.exists(register_path):
with open(register_path, 'r', encoding='utf-8') as f:
@ -491,7 +520,9 @@ async def login(request: LoginRequest):
success=True,
token=token,
message="登录成功",
user_id=user['id']
user_id=user['id'],
username=user['username'],
is_admin=(user['username'] == ADMIN_USERNAME)
)
logger.warning(f"{request.username}】登录失败:用户名或密码错误")
@ -520,7 +551,9 @@ async def login(request: LoginRequest):
success=True,
token=token,
message="登录成功",
user_id=user['id']
user_id=user['id'],
username=user['username'],
is_admin=(user['username'] == ADMIN_USERNAME)
)
logger.warning(f"{request.email}】邮箱登录失败:邮箱或密码错误")
@ -564,7 +597,9 @@ async def login(request: LoginRequest):
success=True,
token=token,
message="登录成功",
user_id=user['id']
user_id=user['id'],
username=user['username'],
is_admin=(user['username'] == ADMIN_USERNAME)
)
else:
@ -581,7 +616,8 @@ async def verify(user_info: Optional[Dict[str, Any]] = Depends(verify_token)):
return {
"authenticated": True,
"user_id": user_info['user_id'],
"username": user_info['username']
"username": user_info['username'],
"is_admin": user_info['username'] == ADMIN_USERNAME
}
return {"authenticated": False}
@ -759,6 +795,15 @@ async def send_verification_code(request: SendCodeRequest):
async def register(request: RegisterRequest):
from db_manager import db_manager
# 检查注册是否开启
registration_enabled = db_manager.get_system_setting('registration_enabled')
if registration_enabled != 'true':
logger.warning(f"{request.username}】注册失败: 注册功能已关闭")
return RegisterResponse(
success=False,
message="注册功能已关闭,请联系管理员"
)
try:
logger.info(f"{request.username}】尝试注册,邮箱: {request.email}")
@ -917,7 +962,8 @@ def get_cookies_details(current_user: Dict[str, Any] = Depends(get_current_user)
'value': cookie_value,
'enabled': cookie_enabled,
'auto_confirm': auto_confirm,
'remark': remark
'remark': remark,
'pause_duration': cookie_details.get('pause_duration', 10) if cookie_details else 10
})
return result
@ -1420,6 +1466,66 @@ def update_system_setting(key: str, setting_data: SystemSettingIn, _: None = Dep
raise HTTPException(status_code=500, detail=str(e))
# ------------------------- 注册设置接口 -------------------------
@app.get('/registration-status')
def get_registration_status():
"""获取注册开关状态(公开接口,无需认证)"""
from db_manager import db_manager
try:
enabled_str = db_manager.get_system_setting('registration_enabled')
logger.info(f"从数据库获取的注册设置值: '{enabled_str}'") # 调试信息
# 如果设置不存在,默认为开启
if enabled_str is None:
enabled_bool = True
message = '注册功能已开启'
else:
enabled_bool = enabled_str == 'true'
message = '注册功能已开启' if enabled_bool else '注册功能已关闭'
logger.info(f"解析后的注册状态: enabled={enabled_bool}, message='{message}'") # 调试信息
return {
'enabled': enabled_bool,
'message': message
}
except Exception as e:
logger.error(f"获取注册状态失败: {e}")
return {'enabled': True, 'message': '注册功能已开启'} # 出错时默认开启
class RegistrationSettingUpdate(BaseModel):
enabled: bool
@app.put('/registration-settings')
def update_registration_settings(setting_data: RegistrationSettingUpdate, admin_user: Dict[str, Any] = Depends(require_admin)):
"""更新注册开关设置(仅管理员)"""
from db_manager import db_manager
try:
enabled = setting_data.enabled
success = db_manager.set_system_setting(
'registration_enabled',
'true' if enabled else 'false',
'是否开启用户注册'
)
if success:
log_with_user('info', f"更新注册设置: {'开启' if enabled else '关闭'}", admin_user)
return {
'success': True,
'enabled': enabled,
'message': f"注册功能已{'开启' if enabled else '关闭'}"
}
else:
raise HTTPException(status_code=500, detail='更新注册设置失败')
except HTTPException:
raise
except Exception as e:
logger.error(f"更新注册设置失败: {e}")
raise HTTPException(status_code=500, detail=str(e))
@ -1452,6 +1558,10 @@ class RemarkUpdate(BaseModel):
remark: str
class PauseDurationUpdate(BaseModel):
pause_duration: int
@app.put("/cookies/{cid}/auto-confirm")
def update_auto_confirm(cid: str, update_data: AutoConfirmUpdate, current_user: Dict[str, Any] = Depends(get_current_user)):
"""更新账号的自动确认发货设置"""
@ -1571,6 +1681,65 @@ def get_cookie_remark(cid: str, current_user: Dict[str, Any] = Depends(get_curre
raise HTTPException(status_code=500, detail=str(e))
@app.put("/cookies/{cid}/pause-duration")
def update_cookie_pause_duration(cid: str, update_data: PauseDurationUpdate, current_user: Dict[str, Any] = Depends(get_current_user)):
"""更新账号自动回复暂停时间"""
if cookie_manager.manager is None:
raise HTTPException(status_code=500, detail="CookieManager 未就绪")
try:
# 检查cookie是否属于当前用户
user_id = current_user['user_id']
from db_manager import db_manager
user_cookies = db_manager.get_all_cookies(user_id)
if cid not in user_cookies:
raise HTTPException(status_code=403, detail="无权限操作该Cookie")
# 验证暂停时间范围1-60分钟
if not (1 <= update_data.pause_duration <= 60):
raise HTTPException(status_code=400, detail="暂停时间必须在1-60分钟之间")
# 更新暂停时间
success = db_manager.update_cookie_pause_duration(cid, update_data.pause_duration)
if success:
log_with_user('info', f"更新账号自动回复暂停时间: {cid} -> {update_data.pause_duration}分钟", current_user)
return {
"message": "暂停时间更新成功",
"pause_duration": update_data.pause_duration
}
else:
raise HTTPException(status_code=500, detail="暂停时间更新失败")
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/cookies/{cid}/pause-duration")
def get_cookie_pause_duration(cid: str, current_user: Dict[str, Any] = Depends(get_current_user)):
"""获取账号自动回复暂停时间"""
if cookie_manager.manager is None:
raise HTTPException(status_code=500, detail="CookieManager 未就绪")
try:
# 检查cookie是否属于当前用户
user_id = current_user['user_id']
from db_manager import db_manager
user_cookies = db_manager.get_all_cookies(user_id)
if cid not in user_cookies:
raise HTTPException(status_code=403, detail="无权限操作该Cookie")
# 获取暂停时间
pause_duration = db_manager.get_cookie_pause_duration(cid)
return {
"pause_duration": pause_duration,
"message": "获取暂停时间成功"
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@ -277,14 +277,21 @@
<table class="table table-hover" id="cookieTable">
<thead>
<tr>
<th style="width: 10%">账号ID</th>
<th style="width: 16%">Cookie值</th>
<th style="width: 8%">关键词</th>
<th style="width: 8%">状态</th>
<th style="width: 9%">默认回复</th>
<th style="width: 9%">AI回复</th>
<th style="width: 10%">自动确认发货</th>
<th style="width: 12%">备注</th>
<th style="width: 9%">账号ID</th>
<th style="width: 14%">Cookie值</th>
<th style="width: 7%">关键词</th>
<th style="width: 7%">状态</th>
<th style="width: 8%">默认回复</th>
<th style="width: 8%">AI回复</th>
<th style="width: 9%">自动确认发货</th>
<th style="width: 10%">备注</th>
<th style="width: 10%">
暂停时间
<i class="bi bi-question-circle ms-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="检测到手动发出消息后,自动回复暂停的时间长度(分钟)。如果在暂停期间再次手动发出消息,会重新开始计时。"></i>
</th>
<th style="width: 18%">操作</th>
</tr>
</thead>
@ -983,6 +990,41 @@
</div>
</div>
<!-- 注册设置 (仅管理员可见) -->
<div id="registration-settings" class="row mt-4" style="display: none;">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="bi bi-person-plus me-2"></i>注册设置
<span class="badge bg-warning ms-2">管理员专用</span>
</div>
<div class="card-body">
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="registrationEnabled">
<label class="form-check-label" for="registrationEnabled">
<strong>开启用户注册</strong>
</label>
</div>
<div class="form-text">
<i class="bi bi-info-circle me-1"></i>
关闭后,新用户将无法注册账号,登录页面也不会显示注册链接
</div>
</div>
<button type="button" class="btn btn-primary" onclick="updateRegistrationSettings()">
<i class="bi bi-check-circle me-1"></i>保存设置
</button>
<div id="registrationStatus" class="mt-3" style="display: none;">
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle me-2"></i>
<span id="registrationStatusText"></span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 备份管理 (仅管理员可见) -->
<div id="backup-management" class="row mt-4" style="display: none;">
<div class="col-12">

View File

@ -76,6 +76,9 @@ function showSection(sectionName) {
case 'message-notifications': // 【消息通知菜单】
loadMessageNotifications();
break;
case 'system-settings': // 【系统设置菜单】
loadSystemSettings();
break;
case 'logs': // 【日志管理菜单】
// 如果没有日志数据,则加载
setTimeout(() => {
@ -1072,7 +1075,7 @@ async function loadCookies() {
if (cookieDetails.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="9" class="text-center py-4 text-muted empty-state">
<td colspan="10" class="text-center py-4 text-muted empty-state">
<i class="bi bi-inbox fs-1 d-block mb-3"></i>
<h5>暂无账号</h5>
<p class="mb-0">请添加新的闲鱼账号开始使用</p>
@ -1206,6 +1209,13 @@ async function loadCookies() {
</span>
</div>
</td>
<td class="align-middle">
<div class="pause-duration-cell" data-cookie-id="${cookie.id}">
<span class="pause-duration-display" onclick="editPauseDuration('${cookie.id}', ${cookie.pause_duration || 10})" title="点击编辑暂停时间" style="cursor: pointer; color: #6c757d; font-size: 0.875rem;">
<i class="bi bi-clock me-1"></i>${cookie.pause_duration || 10}
</span>
</div>
</td>
<td class="align-middle">
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-primary" onclick="editCookieInline('${cookie.id}', '${cookie.value}')" title="修改Cookie" ${!isEnabled ? 'disabled' : ''}>
@ -1244,6 +1254,9 @@ async function loadCookies() {
});
});
// 重新初始化工具提示
initTooltips();
} catch (err) {
// 错误已在fetchJSON中处理
} finally {
@ -1649,7 +1662,7 @@ async function checkAuth() {
}
// 检查是否为管理员,显示管理员菜单和功能
if (result.username === 'admin') {
if (result.is_admin === true) {
const adminMenuSection = document.getElementById('adminMenuSection');
if (adminMenuSection) {
adminMenuSection.style.display = 'block';
@ -1660,6 +1673,12 @@ async function checkAuth() {
if (backupManagement) {
backupManagement.style.display = 'block';
}
// 显示注册设置功能
const registrationSettings = document.getElementById('registration-settings');
if (registrationSettings) {
registrationSettings.style.display = 'block';
}
}
return true;
@ -1771,6 +1790,9 @@ document.addEventListener('DOMContentLoaded', async () => {
// 初始化编辑卡券图片文件选择器
initEditCardImageFileSelector();
// 初始化工具提示
initTooltips();
// 点击侧边栏外部关闭移动端菜单
document.addEventListener('click', function(e) {
const sidebar = document.getElementById('sidebar');
@ -6473,3 +6495,239 @@ function editRemark(cookieId, currentRemark) {
input.focus();
input.select();
}
// 编辑暂停时间
function editPauseDuration(cookieId, currentDuration) {
console.log('editPauseDuration called:', cookieId, currentDuration); // 调试信息
const pauseCell = document.querySelector(`[data-cookie-id="${cookieId}"] .pause-duration-display`);
if (!pauseCell) {
console.log('pauseCell not found'); // 调试信息
return;
}
// 创建输入框
const input = document.createElement('input');
input.type = 'number';
input.className = 'form-control form-control-sm';
input.value = currentDuration || 10;
input.placeholder = '请输入暂停时间...';
input.style.fontSize = '0.875rem';
input.min = 1;
input.max = 60;
input.step = 1;
// 保存原始内容和原始值
const originalContent = pauseCell.innerHTML;
const originalValue = currentDuration || 10;
// 标记是否已经进行了编辑
let hasChanged = false;
let isProcessing = false; // 防止重复处理
// 替换为输入框
pauseCell.innerHTML = '';
pauseCell.appendChild(input);
// 监听输入变化
input.addEventListener('input', () => {
const newValue = parseInt(input.value) || 10;
hasChanged = newValue !== originalValue;
});
// 保存函数
const savePauseDuration = async () => {
console.log('savePauseDuration called, isProcessing:', isProcessing, 'hasChanged:', hasChanged); // 调试信息
if (isProcessing) return; // 防止重复调用
const newDuration = parseInt(input.value) || 10;
console.log('newDuration:', newDuration, 'originalValue:', originalValue); // 调试信息
// 验证范围
if (newDuration < 1 || newDuration > 60) {
showToast('暂停时间必须在1-60分钟之间', 'warning');
input.focus();
return;
}
// 如果没有变化,直接恢复显示
if (!hasChanged || newDuration === originalValue) {
console.log('No changes detected, restoring original content'); // 调试信息
pauseCell.innerHTML = originalContent;
return;
}
isProcessing = true;
try {
const response = await fetch(`${apiBase}/cookies/${cookieId}/pause-duration`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ pause_duration: newDuration })
});
if (response.ok) {
// 更新显示
pauseCell.innerHTML = `
<span class="pause-duration-display" onclick="editPauseDuration('${cookieId}', ${newDuration})" title="点击编辑暂停时间" style="cursor: pointer; color: #6c757d; font-size: 0.875rem;">
<i class="bi bi-clock me-1"></i>${newDuration}
</span>
`;
showToast('暂停时间更新成功', 'success');
} else {
const errorData = await response.json();
showToast(`暂停时间更新失败: ${errorData.detail || '未知错误'}`, 'danger');
// 恢复原始内容
pauseCell.innerHTML = originalContent;
}
} catch (error) {
console.error('更新暂停时间失败:', error);
showToast('暂停时间更新失败', 'danger');
// 恢复原始内容
pauseCell.innerHTML = originalContent;
} finally {
isProcessing = false;
}
};
// 取消函数
const cancelEdit = () => {
if (isProcessing) return;
pauseCell.innerHTML = originalContent;
};
// 延迟绑定blur事件避免立即触发
setTimeout(() => {
input.addEventListener('blur', savePauseDuration);
}, 100);
// 绑定键盘事件
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
savePauseDuration();
} else if (e.key === 'Escape') {
e.preventDefault();
cancelEdit();
}
});
// 聚焦并选中文本
input.focus();
input.select();
}
// ==================== 工具提示初始化 ====================
// 初始化工具提示
function initTooltips() {
// 初始化所有工具提示
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
}
// ==================== 系统设置功能 ====================
// 加载系统设置
async function loadSystemSettings() {
console.log('加载系统设置');
// 通过验证接口获取用户信息(更可靠)
try {
const response = await fetch(`${apiBase}/verify`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const result = await response.json();
const isAdmin = result.is_admin === true;
console.log('用户信息:', result, '是否管理员:', isAdmin);
// 显示/隐藏注册设置(仅管理员可见)
const registrationSettings = document.getElementById('registration-settings');
if (registrationSettings) {
registrationSettings.style.display = isAdmin ? 'block' : 'none';
}
// 如果是管理员,加载注册设置
if (isAdmin) {
await loadRegistrationSettings();
}
}
} catch (error) {
console.error('获取用户信息失败:', error);
// 出错时隐藏管理员功能
const registrationSettings = document.getElementById('registration-settings');
if (registrationSettings) {
registrationSettings.style.display = 'none';
}
}
}
// 加载注册设置
async function loadRegistrationSettings() {
try {
const response = await fetch('/registration-status');
if (response.ok) {
const data = await response.json();
const checkbox = document.getElementById('registrationEnabled');
if (checkbox) {
checkbox.checked = data.enabled;
}
}
} catch (error) {
console.error('加载注册设置失败:', error);
showToast('加载注册设置失败', 'danger');
}
}
// 更新注册设置
async function updateRegistrationSettings() {
const checkbox = document.getElementById('registrationEnabled');
const statusDiv = document.getElementById('registrationStatus');
const statusText = document.getElementById('registrationStatusText');
if (!checkbox) return;
const enabled = checkbox.checked;
try {
const response = await fetch('/registration-settings', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ enabled: enabled })
});
if (response.ok) {
const data = await response.json();
showToast(data.message, 'success');
// 显示状态信息
if (statusDiv && statusText) {
statusText.textContent = data.message;
statusDiv.style.display = 'block';
// 3秒后隐藏状态信息
setTimeout(() => {
statusDiv.style.display = 'none';
}, 3000);
}
} else {
const errorData = await response.json();
showToast(`更新失败: ${errorData.detail || '未知错误'}`, 'danger');
}
} catch (error) {
console.error('更新注册设置失败:', error);
showToast('更新注册设置失败', 'danger');
}
}

View File

@ -218,7 +218,7 @@
</form>
<!-- 注册链接 -->
<div class="text-center mt-3">
<div id="registerSection" class="text-center mt-3">
<span class="text-muted">还没有账号?</span>
<a href="/register.html" class="text-decoration-none">
<i class="bi bi-person-plus me-1"></i>立即注册
@ -377,6 +377,14 @@
// 保存token到localStorage
localStorage.setItem('auth_token', result.token);
// 保存用户信息到localStorage
const userInfo = {
user_id: result.user_id,
username: result.username,
is_admin: result.is_admin
};
localStorage.setItem('user_info', JSON.stringify(userInfo));
// 检查是否有重定向URL
const redirectUrl = localStorage.getItem('redirectAfterLogin');
if (redirectUrl) {
@ -634,8 +642,28 @@
}
}
// 检查注册状态
async function checkRegistrationStatus() {
try {
const response = await fetch('/registration-status');
if (response.ok) {
const data = await response.json();
const registerSection = document.getElementById('registerSection');
if (registerSection) {
registerSection.style.display = data.enabled ? 'block' : 'none';
}
}
} catch (error) {
console.error('检查注册状态失败:', error);
// 出错时默认显示注册链接
}
}
// 事件监听器
document.addEventListener('DOMContentLoaded', function() {
// 检查注册状态
checkRegistrationStatus();
// 登录方式切换
document.querySelectorAll('input[name="loginType"]').forEach(radio => {
radio.addEventListener('change', switchLoginType);