支持多规格发货

This commit is contained in:
zhinianboke 2025-07-31 23:17:33 +08:00
parent 81b1f924cc
commit 1dd4d9b841
6 changed files with 724 additions and 64 deletions

View File

@ -32,10 +32,12 @@
### 🚚 自动发货功能
- **智能匹配** - 基于商品信息自动匹配发货规则
- **多规格支持** - 支持同一商品的不同规格自动匹配对应卡券
- **精确匹配+兜底机制** - 优先精确匹配规格,失败时自动降级到普通卡券
- **延时发货** - 支持设置发货延时时间0-3600秒
- **多种触发** - 支持付款消息、小刀消息等多种触发条件
- **防重复发货** - 智能防重复机制,避免重复发货
- **多种发货方式** - 支持文本内容、卡密文件、API调用等发货方式
- **多种发货方式** - 支持固定文字、批量数据、API调用等发货方式
- **自动确认发货** - 检测到付款后自动调用闲鱼API确认发货
- **防重复确认** - 智能防重复确认机制避免重复API调用
- **发货统计** - 完整的发货记录和统计功能
@ -43,7 +45,8 @@
### 🛍️ 商品管理
- **自动收集** - 消息触发时自动收集商品信息
- **API获取** - 通过闲鱼API获取完整商品详情
- **批量管理** - 支持批量查看、编辑商品信息
- **多规格支持** - 支持多规格商品的规格信息管理
- **批量管理** - 支持批量查看、编辑、切换多规格状态
- **智能去重** - 自动去重,避免重复存储
### 🔍 商品搜索功能
@ -63,6 +66,8 @@
- **模板生成** - 自动生成包含示例数据的导入模板
- **批量操作** - 支持批量添加、更新关键词数据
- **数据验证** - 导入时自动验证数据格式和重复性
- **多规格卡券管理** - 支持创建和管理多规格卡券
- **发货规则管理** - 支持多规格发货规则的创建和管理
- **数据备份** - 自动数据备份和恢复
## 📁 项目结构

View File

@ -1103,7 +1103,7 @@ class XianyuLive:
from utils.order_detail_fetcher import fetch_order_detail_simple
# 获取当前账号的cookie字符串
cookie_string = self.cookie_value
cookie_string = self.cookies_str
logger.debug(f"{self.cookie_id}】使用Cookie长度: {len(cookie_string) if cookie_string else 0}")
# 异步获取订单详情使用当前账号的cookie和无头模式
@ -1236,19 +1236,74 @@ class XianyuLive:
logger.info(f"使用搜索文本匹配发货规则: {search_text[:100]}...")
# 根据商品信息查找匹配的发货规则
delivery_rules = db_manager.get_delivery_rules_by_keyword(search_text)
# 检查商品是否为多规格商品
is_multi_spec = db_manager.get_item_multi_spec_status(self.cookie_id, item_id)
spec_name = None
spec_value = None
# 如果是多规格商品且有订单ID获取规格信息
if is_multi_spec and order_id:
logger.info(f"检测到多规格商品,获取订单规格信息: {order_id}")
try:
order_detail = await self.fetch_order_detail_info(order_id)
if order_detail:
spec_name = order_detail.get('spec_name', '')
spec_value = order_detail.get('spec_value', '')
if spec_name and spec_value:
logger.info(f"获取到规格信息: {spec_name} = {spec_value}")
else:
logger.warning(f"未能获取到规格信息,将使用兜底匹配")
else:
logger.warning(f"获取订单详情失败,将使用兜底匹配")
except Exception as e:
logger.error(f"获取订单规格信息失败: {self._safe_str(e)},将使用兜底匹配")
# 智能匹配发货规则:优先精确匹配,然后兜底匹配
delivery_rules = []
# 第一步:如果有规格信息,尝试精确匹配多规格发货规则
if spec_name and spec_value:
logger.info(f"尝试精确匹配多规格发货规则: {search_text[:50]}... [{spec_name}:{spec_value}]")
delivery_rules = db_manager.get_delivery_rules_by_keyword_and_spec(search_text, spec_name, spec_value)
if delivery_rules:
logger.info(f"✅ 找到精确匹配的多规格发货规则: {len(delivery_rules)}")
else:
logger.info(f"❌ 未找到精确匹配的多规格发货规则")
# 第二步:如果精确匹配失败,尝试兜底匹配(普通发货规则)
if not delivery_rules:
logger.info(f"尝试兜底匹配普通发货规则: {search_text[:50]}...")
delivery_rules = db_manager.get_delivery_rules_by_keyword(search_text)
if delivery_rules:
logger.info(f"✅ 找到兜底匹配的普通发货规则: {len(delivery_rules)}")
else:
logger.info(f"❌ 未找到任何匹配的发货规则")
if not delivery_rules:
logger.info(f"未找到匹配的发货规则: {search_text[:50]}...")
logger.warning(f"未找到匹配的发货规则: {search_text[:50]}...")
return None
# 使用第一个匹配的规则(按关键字长度降序排列,优先匹配更精确的规则)
rule = delivery_rules[0]
# 保存商品信息到数据库
await self.save_item_info_to_db(item_id, search_text)
rule = delivery_rules[0]
logger.info(f"找到匹配的发货规则: {rule['keyword']} -> {rule['card_name']} ({rule['card_type']})")
# 详细的匹配结果日志
if rule.get('is_multi_spec'):
if spec_name and spec_value:
logger.info(f"🎯 精确匹配多规格发货规则: {rule['keyword']} -> {rule['card_name']} [{rule['spec_name']}:{rule['spec_value']}]")
logger.info(f"📋 订单规格: {spec_name}:{spec_value} ✅ 匹配卡券规格: {rule['spec_name']}:{rule['spec_value']}")
else:
logger.info(f"⚠️ 使用多规格发货规则但无订单规格信息: {rule['keyword']} -> {rule['card_name']} [{rule['spec_name']}:{rule['spec_value']}]")
else:
if spec_name and spec_value:
logger.info(f"🔄 兜底匹配普通发货规则: {rule['keyword']} -> {rule['card_name']} ({rule['card_type']})")
logger.info(f"📋 订单规格: {spec_name}:{spec_value} ➡️ 使用普通卡券兜底")
else:
logger.info(f"✅ 匹配普通发货规则: {rule['keyword']} -> {rule['card_name']} ({rule['card_type']})")
# 获取延时设置
delay_seconds = rule.get('card_delay_seconds', 0)
@ -1297,7 +1352,7 @@ class XianyuLive:
elif rule['card_type'] == 'text':
# 固定文字类型:直接使用文字内容
delivery_content = rule['card_text_content']
delivery_content = rule['text_content']
elif rule['card_type'] == 'data':
# 批量数据类型:获取并消费第一条数据
@ -1351,10 +1406,12 @@ class XianyuLive:
try:
import aiohttp
import json
api_config = rule.get('card_api_config')
api_config = rule.get('api_config')
if not api_config:
logger.error("API配置为空")
logger.error(f"API配置为空规则ID: {rule.get('id')}, 卡券名称: {rule.get('card_name')}")
logger.debug(f"规则详情: {rule}")
return None
# 解析API配置

View File

@ -198,6 +198,9 @@ class DBManager:
description TEXT,
enabled BOOLEAN DEFAULT TRUE,
delay_seconds INTEGER DEFAULT 0,
is_multi_spec BOOLEAN DEFAULT FALSE,
spec_name TEXT,
spec_value TEXT,
user_id INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@ -244,6 +247,7 @@ class DBManager:
item_category TEXT,
item_price TEXT,
item_detail TEXT,
is_multi_spec BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE,
@ -412,6 +416,24 @@ class DBManager:
# type列存在更新NULL值
self._execute_sql(cursor, "UPDATE email_verifications SET type = 'register' WHERE type IS NULL")
# 为cards表添加多规格字段如果不存在
try:
self._execute_sql(cursor, "SELECT is_multi_spec FROM cards LIMIT 1")
except sqlite3.OperationalError:
# 多规格字段不存在,需要添加
self._execute_sql(cursor, "ALTER TABLE cards ADD COLUMN is_multi_spec BOOLEAN DEFAULT FALSE")
self._execute_sql(cursor, "ALTER TABLE cards ADD COLUMN spec_name TEXT")
self._execute_sql(cursor, "ALTER TABLE cards ADD COLUMN spec_value TEXT")
logger.info("为cards表添加多规格字段")
# 为item_info表添加多规格字段如果不存在
try:
self._execute_sql(cursor, "SELECT is_multi_spec FROM item_info LIMIT 1")
except sqlite3.OperationalError:
# 多规格字段不存在,需要添加
self._execute_sql(cursor, "ALTER TABLE item_info ADD COLUMN is_multi_spec BOOLEAN DEFAULT FALSE")
logger.info("为item_info表添加多规格字段")
self.conn.commit()
logger.info(f"数据库初始化成功: {self.db_path}")
except Exception as e:
@ -1740,10 +1762,37 @@ class DBManager:
def create_card(self, name: str, card_type: str, api_config=None,
text_content: str = None, data_content: str = None,
description: str = None, enabled: bool = True, delay_seconds: int = 0, user_id: int = None):
"""创建新卡券"""
description: str = None, enabled: bool = True, delay_seconds: int = 0,
is_multi_spec: bool = False, spec_name: str = None, spec_value: str = None,
user_id: int = None):
"""创建新卡券(支持多规格)"""
with self.lock:
try:
# 验证多规格参数
if is_multi_spec:
if not spec_name or not spec_value:
raise ValueError("多规格卡券必须提供规格名称和规格值")
# 检查唯一性:卡券名称+规格名称+规格值
cursor = self.conn.cursor()
cursor.execute('''
SELECT COUNT(*) FROM cards
WHERE name = ? AND spec_name = ? AND spec_value = ? AND user_id = ?
''', (name, spec_name, spec_value, user_id))
if cursor.fetchone()[0] > 0:
raise ValueError(f"卡券已存在:{name} - {spec_name}:{spec_value}")
else:
# 检查唯一性:仅卡券名称
cursor = self.conn.cursor()
cursor.execute('''
SELECT COUNT(*) FROM cards
WHERE name = ? AND (is_multi_spec = 0 OR is_multi_spec IS NULL) AND user_id = ?
''', (name, user_id))
if cursor.fetchone()[0] > 0:
raise ValueError(f"卡券名称已存在:{name}")
# 处理api_config参数 - 如果是字典则转换为JSON字符串
api_config_str = None
if api_config is not None:
@ -1753,16 +1802,21 @@ class DBManager:
else:
api_config_str = str(api_config)
cursor = self.conn.cursor()
cursor.execute('''
INSERT INTO cards (name, type, api_config, text_content, data_content,
description, enabled, delay_seconds, user_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
description, enabled, delay_seconds, is_multi_spec,
spec_name, spec_value, user_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (name, card_type, api_config_str, text_content, data_content,
description, enabled, delay_seconds, user_id))
description, enabled, delay_seconds, is_multi_spec,
spec_name, spec_value, user_id))
self.conn.commit()
card_id = cursor.lastrowid
logger.info(f"创建卡券成功: {name} (ID: {card_id})")
if is_multi_spec:
logger.info(f"创建多规格卡券成功: {name} - {spec_name}:{spec_value} (ID: {card_id})")
else:
logger.info(f"创建卡券成功: {name} (ID: {card_id})")
return card_id
except Exception as e:
logger.error(f"创建卡券失败: {e}")
@ -1776,7 +1830,8 @@ class DBManager:
if user_id is not None:
cursor.execute('''
SELECT id, name, type, api_config, text_content, data_content,
description, enabled, delay_seconds, created_at, updated_at
description, enabled, delay_seconds, is_multi_spec,
spec_name, spec_value, created_at, updated_at
FROM cards
WHERE user_id = ?
ORDER BY created_at DESC
@ -1784,7 +1839,8 @@ class DBManager:
else:
cursor.execute('''
SELECT id, name, type, api_config, text_content, data_content,
description, enabled, delay_seconds, created_at, updated_at
description, enabled, delay_seconds, is_multi_spec,
spec_name, spec_value, created_at, updated_at
FROM cards
ORDER BY created_at DESC
''')
@ -1811,8 +1867,11 @@ class DBManager:
'description': row[6],
'enabled': bool(row[7]),
'delay_seconds': row[8] or 0,
'created_at': row[9],
'updated_at': row[10]
'is_multi_spec': bool(row[9]) if row[9] is not None else False,
'spec_name': row[10],
'spec_value': row[11],
'created_at': row[12],
'updated_at': row[13]
})
return cards
@ -1828,13 +1887,15 @@ class DBManager:
if user_id is not None:
cursor.execute('''
SELECT id, name, type, api_config, text_content, data_content,
description, enabled, delay_seconds, created_at, updated_at
description, enabled, delay_seconds, is_multi_spec,
spec_name, spec_value, created_at, updated_at
FROM cards WHERE id = ? AND user_id = ?
''', (card_id, user_id))
else:
cursor.execute('''
SELECT id, name, type, api_config, text_content, data_content,
description, enabled, delay_seconds, created_at, updated_at
description, enabled, delay_seconds, is_multi_spec,
spec_name, spec_value, created_at, updated_at
FROM cards WHERE id = ?
''', (card_id,))
@ -1860,8 +1921,11 @@ class DBManager:
'description': row[6],
'enabled': bool(row[7]),
'delay_seconds': row[8] or 0,
'created_at': row[9],
'updated_at': row[10]
'is_multi_spec': bool(row[9]) if row[9] is not None else False,
'spec_name': row[10],
'spec_value': row[11],
'created_at': row[12],
'updated_at': row[13]
}
return None
except Exception as e:
@ -1870,7 +1934,8 @@ class DBManager:
def update_card(self, card_id: int, name: str = None, card_type: str = None,
api_config=None, text_content: str = None, data_content: str = None,
description: str = None, enabled: bool = None, delay_seconds: int = None):
description: str = None, enabled: bool = None, delay_seconds: int = None,
is_multi_spec: bool = None, spec_name: str = None, spec_value: str = None):
"""更新卡券"""
with self.lock:
try:
@ -1913,6 +1978,15 @@ class DBManager:
if delay_seconds is not None:
update_fields.append("delay_seconds = ?")
params.append(delay_seconds)
if is_multi_spec is not None:
update_fields.append("is_multi_spec = ?")
params.append(is_multi_spec)
if spec_name is not None:
update_fields.append("spec_name = ?")
params.append(spec_name)
if spec_value is not None:
update_fields.append("spec_value = ?")
params.append(spec_value)
if not update_fields:
return True # 没有需要更新的字段
@ -1964,7 +2038,8 @@ class DBManager:
cursor.execute('''
SELECT dr.id, dr.keyword, dr.card_id, dr.delivery_count, dr.enabled,
dr.description, dr.delivery_times, dr.created_at, dr.updated_at,
c.name as card_name, c.type as card_type
c.name as card_name, c.type as card_type,
c.is_multi_spec, c.spec_name, c.spec_value
FROM delivery_rules dr
LEFT JOIN cards c ON dr.card_id = c.id
WHERE dr.user_id = ?
@ -1974,7 +2049,8 @@ class DBManager:
cursor.execute('''
SELECT dr.id, dr.keyword, dr.card_id, dr.delivery_count, dr.enabled,
dr.description, dr.delivery_times, dr.created_at, dr.updated_at,
c.name as card_name, c.type as card_type
c.name as card_name, c.type as card_type,
c.is_multi_spec, c.spec_name, c.spec_value
FROM delivery_rules dr
LEFT JOIN cards c ON dr.card_id = c.id
ORDER BY dr.created_at DESC
@ -1993,7 +2069,10 @@ class DBManager:
'created_at': row[7],
'updated_at': row[8],
'card_name': row[9],
'card_type': row[10]
'card_type': row[10],
'is_multi_spec': bool(row[11]) if row[11] is not None else False,
'spec_name': row[12],
'spec_value': row[13]
})
return rules
@ -2047,9 +2126,9 @@ class DBManager:
'delivery_times': row[6],
'card_name': row[7],
'card_type': row[8],
'card_api_config': api_config,
'card_text_content': row[10],
'card_data_content': row[11],
'api_config': api_config, # 修复字段名
'text_content': row[10],
'data_content': row[11],
'card_enabled': bool(row[12]),
'card_description': row[13], # 卡券备注信息
'card_delay_seconds': row[14] or 0 # 延时秒数
@ -2173,6 +2252,136 @@ class DBManager:
except Exception as e:
logger.error(f"更新发货次数失败: {e}")
def get_delivery_rules_by_keyword_and_spec(self, keyword: str, spec_name: str = None, spec_value: str = None):
"""根据关键字和规格信息获取匹配的发货规则(支持多规格)"""
with self.lock:
try:
cursor = self.conn.cursor()
# 优先匹配:卡券名称+规格名称+规格值
if spec_name and spec_value:
cursor.execute('''
SELECT dr.id, dr.keyword, dr.card_id, dr.delivery_count, dr.enabled,
dr.description, dr.delivery_times,
c.name as card_name, c.type as card_type, c.api_config,
c.text_content, c.data_content, c.enabled as card_enabled,
c.description as card_description, c.delay_seconds as card_delay_seconds,
c.is_multi_spec, c.spec_name, c.spec_value
FROM delivery_rules dr
LEFT JOIN cards c ON dr.card_id = c.id
WHERE dr.enabled = 1 AND c.enabled = 1
AND (? LIKE '%' || dr.keyword || '%' OR dr.keyword LIKE '%' || ? || '%')
AND c.is_multi_spec = 1 AND c.spec_name = ? AND c.spec_value = ?
ORDER BY
CASE
WHEN ? LIKE '%' || dr.keyword || '%' THEN LENGTH(dr.keyword)
ELSE LENGTH(dr.keyword) / 2
END DESC,
dr.delivery_times ASC
''', (keyword, keyword, spec_name, spec_value, keyword))
rules = []
for row in cursor.fetchall():
# 解析api_config JSON字符串
api_config = row[9]
if api_config:
try:
import json
api_config = json.loads(api_config)
except (json.JSONDecodeError, TypeError):
# 如果解析失败,保持原始字符串
pass
rules.append({
'id': row[0],
'keyword': row[1],
'card_id': row[2],
'delivery_count': row[3],
'enabled': bool(row[4]),
'description': row[5],
'delivery_times': row[6] or 0,
'card_name': row[7],
'card_type': row[8],
'api_config': api_config,
'text_content': row[10],
'data_content': row[11],
'card_enabled': bool(row[12]),
'card_description': row[13],
'card_delay_seconds': row[14] or 0,
'is_multi_spec': bool(row[15]),
'spec_name': row[16],
'spec_value': row[17]
})
if rules:
logger.info(f"找到多规格匹配规则: {keyword} - {spec_name}:{spec_value}")
return rules
# 兜底匹配:仅卡券名称
cursor.execute('''
SELECT dr.id, dr.keyword, dr.card_id, dr.delivery_count, dr.enabled,
dr.description, dr.delivery_times,
c.name as card_name, c.type as card_type, c.api_config,
c.text_content, c.data_content, c.enabled as card_enabled,
c.description as card_description, c.delay_seconds as card_delay_seconds,
c.is_multi_spec, c.spec_name, c.spec_value
FROM delivery_rules dr
LEFT JOIN cards c ON dr.card_id = c.id
WHERE dr.enabled = 1 AND c.enabled = 1
AND (? LIKE '%' || dr.keyword || '%' OR dr.keyword LIKE '%' || ? || '%')
AND (c.is_multi_spec = 0 OR c.is_multi_spec IS NULL)
ORDER BY
CASE
WHEN ? LIKE '%' || dr.keyword || '%' THEN LENGTH(dr.keyword)
ELSE LENGTH(dr.keyword) / 2
END DESC,
dr.delivery_times ASC
''', (keyword, keyword, keyword))
rules = []
for row in cursor.fetchall():
# 解析api_config JSON字符串
api_config = row[9]
if api_config:
try:
import json
api_config = json.loads(api_config)
except (json.JSONDecodeError, TypeError):
# 如果解析失败,保持原始字符串
pass
rules.append({
'id': row[0],
'keyword': row[1],
'card_id': row[2],
'delivery_count': row[3],
'enabled': bool(row[4]),
'description': row[5],
'delivery_times': row[6] or 0,
'card_name': row[7],
'card_type': row[8],
'api_config': api_config,
'text_content': row[10],
'data_content': row[11],
'card_enabled': bool(row[12]),
'card_description': row[13],
'card_delay_seconds': row[14] or 0,
'is_multi_spec': bool(row[15]) if row[15] is not None else False,
'spec_name': row[16],
'spec_value': row[17]
})
if rules:
logger.info(f"找到兜底匹配规则: {keyword}")
else:
logger.info(f"未找到匹配规则: {keyword}")
return rules
except Exception as e:
logger.error(f"获取发货规则失败: {e}")
return []
def delete_card(self, card_id: int):
"""删除卡券"""
with self.lock:
@ -2463,6 +2672,49 @@ class DBManager:
logger.error(f"获取商品信息失败: {e}")
return None
def update_item_multi_spec_status(self, cookie_id: str, item_id: str, is_multi_spec: bool) -> bool:
"""更新商品的多规格状态"""
try:
with self.lock:
cursor = self.conn.cursor()
cursor.execute('''
UPDATE item_info
SET is_multi_spec = ?, updated_at = CURRENT_TIMESTAMP
WHERE cookie_id = ? AND item_id = ?
''', (is_multi_spec, cookie_id, item_id))
if cursor.rowcount > 0:
self.conn.commit()
logger.info(f"更新商品多规格状态成功: {item_id} -> {is_multi_spec}")
return True
else:
logger.warning(f"商品不存在,无法更新多规格状态: {item_id}")
return False
except Exception as e:
logger.error(f"更新商品多规格状态失败: {e}")
self.conn.rollback()
return False
def get_item_multi_spec_status(self, cookie_id: str, item_id: str) -> bool:
"""获取商品的多规格状态"""
try:
with self.lock:
cursor = self.conn.cursor()
cursor.execute('''
SELECT is_multi_spec FROM item_info
WHERE cookie_id = ? AND item_id = ?
''', (cookie_id, item_id))
row = cursor.fetchone()
if row:
return bool(row[0]) if row[0] is not None else False
return False
except Exception as e:
logger.error(f"获取商品多规格状态失败: {e}")
return False
def get_items_by_cookie(self, cookie_id: str) -> List[Dict]:
"""获取指定Cookie的所有商品信息

View File

@ -1737,6 +1737,12 @@ def create_card(card_data: dict, current_user: Dict[str, Any] = Depends(get_curr
log_with_user('info', f"创建卡券: {card_name}", current_user)
# 验证多规格字段
is_multi_spec = card_data.get('is_multi_spec', False)
if is_multi_spec:
if not card_data.get('spec_name') or not card_data.get('spec_value'):
raise HTTPException(status_code=400, detail="多规格卡券必须提供规格名称和规格值")
card_id = db_manager.create_card(
name=card_data.get('name'),
card_type=card_data.get('type'),
@ -1746,6 +1752,9 @@ def create_card(card_data: dict, current_user: Dict[str, Any] = Depends(get_curr
description=card_data.get('description'),
enabled=card_data.get('enabled', True),
delay_seconds=card_data.get('delay_seconds', 0),
is_multi_spec=is_multi_spec,
spec_name=card_data.get('spec_name') if is_multi_spec else None,
spec_value=card_data.get('spec_value') if is_multi_spec else None,
user_id=user_id
)
@ -1776,6 +1785,12 @@ def update_card(card_id: int, card_data: dict, _: None = Depends(require_auth)):
"""更新卡券"""
try:
from db_manager import db_manager
# 验证多规格字段
is_multi_spec = card_data.get('is_multi_spec')
if is_multi_spec:
if not card_data.get('spec_name') or not card_data.get('spec_value'):
raise HTTPException(status_code=400, detail="多规格卡券必须提供规格名称和规格值")
success = db_manager.update_card(
card_id=card_id,
name=card_data.get('name'),
@ -1785,7 +1800,10 @@ def update_card(card_id: int, card_data: dict, _: None = Depends(require_auth)):
data_content=card_data.get('data_content'),
description=card_data.get('description'),
enabled=card_data.get('enabled', True),
delay_seconds=card_data.get('delay_seconds')
delay_seconds=card_data.get('delay_seconds'),
is_multi_spec=is_multi_spec,
spec_name=card_data.get('spec_name'),
spec_value=card_data.get('spec_value')
)
if success:
return {"message": "卡券更新成功"}
@ -3039,5 +3057,25 @@ def clear_table_data(table_name: str, admin_user: Dict[str, Any] = Depends(requi
raise HTTPException(status_code=500, detail=str(e))
# 商品多规格管理API
@app.put("/items/{cookie_id}/{item_id}/multi-spec")
def update_item_multi_spec(cookie_id: str, item_id: str, spec_data: dict, _: None = Depends(require_auth)):
"""更新商品的多规格状态"""
try:
from db_manager import db_manager
is_multi_spec = spec_data.get('is_multi_spec', False)
success = db_manager.update_item_multi_spec_status(cookie_id, item_id, is_multi_spec)
if success:
return {"message": f"商品多规格状态已{'开启' if is_multi_spec else '关闭'}"}
else:
raise HTTPException(status_code=404, detail="商品不存在")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8080)

View File

@ -2,7 +2,7 @@
# 闲鱼自动回复系统 - Python依赖包
# ================================
# Web框架和API相关
# 核心Web框架
fastapi>=0.111.0
uvicorn[standard]>=0.29.0
pydantic>=2.7.0
@ -11,11 +11,13 @@ pydantic>=2.7.0
loguru>=0.7.0
# 网络通信
websockets>=10.0,<13.0
websockets>=12.0
aiohttp>=3.9.0
requests>=2.31.0
# 配置文件处理
PyYAML>=6.0.0
python-dotenv>=1.0.1
# JavaScript执行引擎
PyExecJS>=1.5.1
@ -26,26 +28,21 @@ blackboxprotobuf>=1.0.1
# 系统监控
psutil>=5.9.0
# HTTP客户端
requests>=2.31.0
# 文件上传支持
python-multipart>=0.0.6
# AI回复相关
# AI回复引擎
openai>=1.65.5
python-dotenv>=1.0.1
# 图像处理(图形验证码)
# 图像处理(验证码生成
Pillow>=10.0.0
# 浏览器自动化(商品搜索功能
# 浏览器自动化(商品搜索、订单详情获取
playwright>=1.40.0
# 加密和安全
PyJWT>=2.8.0
passlib>=1.7.4
bcrypt>=4.0.1
passlib[bcrypt]>=1.7.4
cryptography>=41.0.0
# 时间处理
@ -54,7 +51,7 @@ python-dateutil>=2.8.2
# 正则表达式增强
regex>=2023.10.3
# Excel文件处理导入导出功能
# Excel文件处理数据导入导出)
pandas>=2.0.0
openpyxl>=3.1.0

View File

@ -1419,12 +1419,13 @@
<th style="width: 5%">
<input type="checkbox" id="selectAllItems" onchange="toggleSelectAll(this)">
</th>
<th style="width: 15%">账号ID</th>
<th style="width: 15%">商品ID</th>
<th style="width: 20%">商品标题</th>
<th style="width: 25%">商品详情</th>
<th style="width: 12%">更新时间</th>
<th style="width: 8%">操作</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: 8%">多规格</th>
<th style="width: 10%">更新时间</th>
<th style="width: 15%">操作</th>
</tr>
</thead>
<tbody id="itemsTableBody">
@ -1567,6 +1568,7 @@
<tr>
<th>卡券名称</th>
<th>类型</th>
<th>规格信息</th>
<th>数据量</th>
<th>延时时间</th>
<th>状态</th>
@ -2157,7 +2159,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addCardForm">
<form id="addCardForm" onsubmit="event.preventDefault(); saveCard(); return false;">
<div class="mb-3">
<label class="form-label">卡券名称 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="cardName" placeholder="例如:游戏点卡、会员卡等" required>
@ -2253,6 +2255,46 @@
备注内容会与发货内容一起发送。使用 <code>{DELIVERY_CONTENT}</code> 变量可以在备注中插入实际的发货内容。
</small>
</div>
<!-- 多规格设置 -->
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="isMultiSpec" onchange="toggleMultiSpecFields()">
<label class="form-check-label" for="isMultiSpec">
<strong>多规格卡券</strong>
</label>
</div>
<div class="form-text">开启后可以为同一商品的不同规格创建不同的卡券</div>
</div>
<!-- 多规格字段 -->
<div id="multiSpecFields" style="display: none;">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">规格名称 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="specName" placeholder="例如:套餐类型、颜色、尺寸">
<div class="form-text">规格的名称,如套餐类型、颜色等</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">规格值 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="specValue" placeholder="例如30天、红色、XL">
<div class="form-text">具体的规格值如30天、红色等</div>
</div>
</div>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
<strong>多规格说明:</strong>
<ul class="mb-0 mt-2">
<li>同一卡券名称可以创建多个不同规格的卡券</li>
<li>卡券名称+规格名称+规格值必须唯一</li>
<li>自动发货时会优先匹配精确规格,找不到时使用普通卡券兜底</li>
</ul>
</div>
</div>
</form>
</div>
<div class="modal-footer">
@ -2380,6 +2422,46 @@
备注内容会与发货内容一起发送。使用 <code>{DELIVERY_CONTENT}</code> 变量可以在备注中插入实际的发货内容。
</small>
</div>
<!-- 多规格设置 -->
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="editIsMultiSpec" onchange="toggleEditMultiSpecFields()">
<label class="form-check-label" for="editIsMultiSpec">
<strong>多规格卡券</strong>
</label>
</div>
<div class="form-text">开启后可以为同一商品的不同规格创建不同的卡券</div>
</div>
<!-- 多规格字段 -->
<div id="editMultiSpecFields" style="display: none;">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">规格名称 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="editSpecName" placeholder="例如:套餐类型、颜色、尺寸">
<div class="form-text">规格的名称,如套餐类型、颜色等</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">规格值 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="editSpecValue" placeholder="例如30天、红色、XL">
<div class="form-text">具体的规格值如30天、红色等</div>
</div>
</div>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
<strong>多规格说明:</strong>
<ul class="mb-0 mt-2">
<li>同一卡券名称可以创建多个不同规格的卡券</li>
<li>卡券名称+规格名称+规格值必须唯一</li>
<li>自动发货时会优先匹配精确规格,找不到时使用普通卡券兜底</li>
</ul>
</div>
</div>
</form>
</div>
<div class="modal-footer">
@ -5021,7 +5103,7 @@
if (cards.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="7" class="text-center py-4 text-muted">
<td colspan="8" class="text-center py-4 text-muted">
<i class="bi bi-credit-card fs-1 d-block mb-3"></i>
<h5>暂无卡券数据</h5>
<p class="mb-0">点击"添加卡券"开始创建您的第一个卡券</p>
@ -5071,12 +5153,19 @@
`${card.delay_seconds}秒` :
'<span class="text-muted">立即</span>';
// 规格信息显示
let specDisplay = '<span class="text-muted">普通卡券</span>';
if (card.is_multi_spec && card.spec_name && card.spec_value) {
specDisplay = `<span class="badge bg-primary">${card.spec_name}: ${card.spec_value}</span>`;
}
tr.innerHTML = `
<td>
<div class="fw-bold">${card.name}</div>
${card.description ? `<small class="text-muted">${card.description}</small>` : ''}
</td>
<td>${typeBadge}</td>
<td>${specDisplay}</td>
<td>${dataCount}</td>
<td>${delayDisplay}</td>
<td>${statusBadge}</td>
@ -5132,6 +5221,93 @@
document.getElementById('dataFields').style.display = cardType === 'data' ? 'block' : 'none';
}
// 切换多规格字段显示
function toggleMultiSpecFields() {
const isMultiSpec = document.getElementById('isMultiSpec').checked;
document.getElementById('multiSpecFields').style.display = isMultiSpec ? 'block' : 'none';
}
// 切换编辑多规格字段显示
function toggleEditMultiSpecFields() {
const checkbox = document.getElementById('editIsMultiSpec');
const fieldsDiv = document.getElementById('editMultiSpecFields');
if (!checkbox) {
console.error('编辑多规格开关元素未找到');
return;
}
if (!fieldsDiv) {
console.error('编辑多规格字段容器未找到');
return;
}
const isMultiSpec = checkbox.checked;
const displayStyle = isMultiSpec ? 'block' : 'none';
console.log('toggleEditMultiSpecFields - 多规格状态:', isMultiSpec);
console.log('toggleEditMultiSpecFields - 设置显示样式:', displayStyle);
fieldsDiv.style.display = displayStyle;
// 验证设置是否生效
console.log('toggleEditMultiSpecFields - 实际显示样式:', fieldsDiv.style.display);
}
// 清空添加卡券表单
function clearAddCardForm() {
try {
// 安全地清空表单字段
const setElementValue = (id, value) => {
const element = document.getElementById(id);
if (element) {
if (element.type === 'checkbox') {
element.checked = value;
} else {
element.value = value;
}
} else {
console.warn(`Element with id '${id}' not found`);
}
};
const setElementDisplay = (id, display) => {
const element = document.getElementById(id);
if (element) {
element.style.display = display;
} else {
console.warn(`Element with id '${id}' not found`);
}
};
// 清空基本字段
setElementValue('cardName', '');
setElementValue('cardType', 'text');
setElementValue('cardDescription', '');
setElementValue('cardDelaySeconds', '0');
setElementValue('isMultiSpec', false);
setElementValue('specName', '');
setElementValue('specValue', '');
// 隐藏多规格字段
setElementDisplay('multiSpecFields', 'none');
// 清空类型相关字段
setElementValue('textContent', '');
setElementValue('dataContent', '');
setElementValue('apiUrl', '');
setElementValue('apiMethod', 'GET');
setElementValue('apiHeaders', '');
setElementValue('apiParams', '');
setElementValue('apiTimeout', '10');
// 重置字段显示
toggleCardTypeFields();
} catch (error) {
console.error('清空表单时出错:', error);
}
}
// 保存卡券
async function saveCard() {
try {
@ -5143,12 +5319,26 @@
return;
}
// 检查多规格设置
const isMultiSpec = document.getElementById('isMultiSpec').checked;
const specName = document.getElementById('specName').value;
const specValue = document.getElementById('specValue').value;
// 验证多规格字段
if (isMultiSpec && (!specName || !specValue)) {
showToast('多规格卡券必须填写规格名称和规格值', 'warning');
return;
}
const cardData = {
name: cardName,
type: cardType,
description: document.getElementById('cardDescription').value,
delay_seconds: parseInt(document.getElementById('cardDelaySeconds').value) || 0,
enabled: true
enabled: true,
is_multi_spec: isMultiSpec,
spec_name: isMultiSpec ? specName : null,
spec_value: isMultiSpec ? specValue : null
};
// 根据类型添加特定配置
@ -5208,14 +5398,28 @@
if (response.ok) {
showToast('卡券保存成功', 'success');
bootstrap.Modal.getInstance(document.getElementById('addCardModal')).hide();
// 清空表单
clearAddCardForm();
loadCards();
} else {
const error = await response.text();
showToast(`保存失败: ${error}`, 'danger');
let errorMessage = '保存失败';
try {
const errorData = await response.json();
errorMessage = errorData.error || errorData.detail || errorMessage;
} catch (e) {
// 如果不是JSON格式尝试获取文本
try {
const errorText = await response.text();
errorMessage = errorText || errorMessage;
} catch (e2) {
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
}
}
showToast(`保存失败: ${errorMessage}`, 'danger');
}
} catch (error) {
console.error('保存卡券失败:', error);
showToast('保存卡券失败', 'danger');
showToast(`网络错误: ${error.message}`, 'danger');
}
}
// ==================== 自动发货功能 ====================
@ -5294,7 +5498,12 @@
${rule.description ? `<small class="text-muted">${rule.description}</small>` : ''}
</td>
<td>
<span class="badge bg-primary">${rule.card_name || '未知卡券'}</span>
<div>
<span class="badge bg-primary">${rule.card_name || '未知卡券'}</span>
${rule.is_multi_spec && rule.spec_name && rule.spec_value ?
`<br><small class="text-muted mt-1 d-block"><i class="bi bi-tags"></i> ${rule.spec_name}: ${rule.spec_value}</small>` :
''}
</div>
</td>
<td>${cardTypeBadge}</td>
<td>
@ -5364,7 +5573,20 @@
if (card.enabled) { // 只显示启用的卡券
const option = document.createElement('option');
option.value = card.id;
option.textContent = `${card.name} (${card.type === 'api' ? 'API' : card.type === 'text' ? '固定文字' : '批量数据'})`;
// 构建显示文本
let displayText = card.name;
// 添加类型信息
const typeText = card.type === 'api' ? 'API' : card.type === 'text' ? '固定文字' : '批量数据';
displayText += ` (${typeText})`;
// 添加规格信息
if (card.is_multi_spec && card.spec_name && card.spec_value) {
displayText += ` [${card.spec_name}:${card.spec_value}]`;
}
option.textContent = displayText;
select.appendChild(option);
}
});
@ -5440,6 +5662,17 @@
document.getElementById('editCardDelaySeconds').value = card.delay_seconds || 0;
document.getElementById('editCardEnabled').checked = card.enabled;
// 填充多规格字段
const isMultiSpec = card.is_multi_spec || false;
document.getElementById('editIsMultiSpec').checked = isMultiSpec;
document.getElementById('editSpecName').value = card.spec_name || '';
document.getElementById('editSpecValue').value = card.spec_value || '';
// 添加调试日志
console.log('编辑卡券 - 多规格状态:', isMultiSpec);
console.log('编辑卡券 - 规格名称:', card.spec_name);
console.log('编辑卡券 - 规格值:', card.spec_value);
// 根据类型填充特定字段
if (card.type === 'api' && card.api_config) {
document.getElementById('editApiUrl').value = card.api_config.url || '';
@ -5456,6 +5689,19 @@
// 显示对应的字段
toggleEditCardTypeFields();
// 使用延迟调用确保DOM更新完成后再显示多规格字段
setTimeout(() => {
console.log('延迟调用 toggleEditMultiSpecFields');
toggleEditMultiSpecFields();
// 验证多规格字段是否正确显示
const multiSpecElement = document.getElementById('editMultiSpecFields');
const isChecked = document.getElementById('editIsMultiSpec').checked;
console.log('多规格元素存在:', !!multiSpecElement);
console.log('多规格开关状态:', isChecked);
console.log('多规格字段显示状态:', multiSpecElement ? multiSpecElement.style.display : 'element not found');
}, 100);
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('editCardModal'));
modal.show();
@ -5489,12 +5735,26 @@
return;
}
// 检查多规格设置
const isMultiSpec = document.getElementById('editIsMultiSpec').checked;
const specName = document.getElementById('editSpecName').value;
const specValue = document.getElementById('editSpecValue').value;
// 验证多规格字段
if (isMultiSpec && (!specName || !specValue)) {
showToast('多规格卡券必须填写规格名称和规格值', 'warning');
return;
}
const cardData = {
name: cardName,
type: cardType,
description: document.getElementById('editCardDescription').value,
delay_seconds: parseInt(document.getElementById('editCardDelaySeconds').value) || 0,
enabled: document.getElementById('editCardEnabled').checked
enabled: document.getElementById('editCardEnabled').checked,
is_multi_spec: isMultiSpec,
spec_name: isMultiSpec ? specName : null,
spec_value: isMultiSpec ? specValue : null
};
// 根据类型添加特定配置
@ -5651,7 +5911,20 @@
if (card.enabled) { // 只显示启用的卡券
const option = document.createElement('option');
option.value = card.id;
option.textContent = `${card.name} (${card.type === 'api' ? 'API' : card.type === 'text' ? '固定文字' : '批量数据'})`;
// 构建显示文本
let displayText = card.name;
// 添加类型信息
const typeText = card.type === 'api' ? 'API' : card.type === 'text' ? '固定文字' : '批量数据';
displayText += ` (${typeText})`;
// 添加规格信息
if (card.is_multi_spec && card.spec_name && card.spec_value) {
displayText += ` [${card.spec_name}:${card.spec_value}]`;
}
option.textContent = displayText;
select.appendChild(option);
}
});
@ -6316,6 +6589,34 @@
// ==================== 商品管理功能 ====================
// 切换商品多规格状态
async function toggleItemMultiSpec(cookieId, itemId, isMultiSpec) {
try {
const response = await fetch(`${apiBase}/items/${encodeURIComponent(cookieId)}/${encodeURIComponent(itemId)}/multi-spec`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
is_multi_spec: isMultiSpec
})
});
if (response.ok) {
showToast(`${isMultiSpec ? '开启' : '关闭'}多规格成功`, 'success');
// 刷新商品列表
await refreshItemsData();
} else {
const errorData = await response.json();
throw new Error(errorData.error || '操作失败');
}
} catch (error) {
console.error('切换多规格状态失败:', error);
showToast(`切换多规格状态失败: ${error.message}`, 'danger');
}
}
// 加载商品列表
async function loadItems() {
try {
@ -6475,7 +6776,7 @@
const tbody = document.getElementById('itemsTableBody');
if (!items || items.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted">暂无商品数据</td></tr>';
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">暂无商品数据</td></tr>';
// 重置选择状态
const selectAllCheckbox = document.getElementById('selectAllItems');
if (selectAllCheckbox) {
@ -6511,6 +6812,12 @@
}
}
// 多规格状态显示
const isMultiSpec = item.is_multi_spec;
const multiSpecDisplay = isMultiSpec ?
'<span class="badge bg-success">多规格</span>' :
'<span class="badge bg-secondary">普通</span>';
return `
<tr>
<td>
@ -6523,6 +6830,7 @@
<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>${multiSpecDisplay}</td>
<td>${formatDateTime(item.updated_at)}</td>
<td>
<div class="btn-group" role="group">
@ -6532,6 +6840,9 @@
<button class="btn btn-sm btn-outline-danger" onclick="deleteItem('${escapeHtml(item.cookie_id)}', '${escapeHtml(item.item_id)}', '${escapeHtml(item.item_title || item.item_id)}')" title="删除">
<i class="bi bi-trash"></i>
</button>
<button class="btn btn-sm ${isMultiSpec ? 'btn-warning' : 'btn-success'}" onclick="toggleItemMultiSpec('${escapeHtml(item.cookie_id)}', '${escapeHtml(item.item_id)}', ${!isMultiSpec})" title="${isMultiSpec ? '关闭多规格' : '开启多规格'}">
<i class="bi ${isMultiSpec ? 'bi-toggle-on' : 'bi-toggle-off'}"></i>
</button>
</div>
</td>
</tr>