feat: 租户管理体系建设 CRUD + 各业务模块接入 tenant_id
1. 新增 Tenant 模型(tenants 表),支持租户创建、重命名、删除 2. 新增 /api/tenants CRUD 蓝图,default 租户不可删除 3. 数据库初始化时自动创建默认租户记录 4. Dashboard 新增租户管理标签页(创建/编辑/删除租户) 5. 各业务模块写入数据时正确传递 tenant_id: - realtime_chat: create_session 和 _save_conversation 支持 tenant_id - dialogue_manager: _save_conversation 和 create_work_order 支持 tenant_id - conversation_history: save_conversation 支持 tenant_id - workorder_sync: sync_from_feishu 支持 tenant_id - websocket_server: create_session 传递 tenant_id - HTTP chat API: create_session 传递 tenant_id - feishu_sync API: 同步时传递 tenant_id - workorders API: 创建工单时传递 tenant_id 6. 网页对话入口添加租户选择器 7. 知识库搜索按租户隔离(realtime_chat 中 _search_knowledge 传递 tenant_id) 8. 初始化时自动加载租户列表填充选择器
This commit is contained in:
Binary file not shown.
@@ -68,10 +68,35 @@ class DatabaseManager:
|
||||
Base.metadata.create_all(bind=self.engine)
|
||||
logger.info("数据库初始化成功")
|
||||
|
||||
# 确保默认租户存在
|
||||
self._ensure_default_tenant()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"数据库初始化失败: {e}")
|
||||
raise
|
||||
|
||||
def _ensure_default_tenant(self):
|
||||
"""确保默认租户记录存在"""
|
||||
try:
|
||||
from .models import Tenant, DEFAULT_TENANT
|
||||
session = self.SessionLocal()
|
||||
try:
|
||||
existing = session.query(Tenant).filter(Tenant.tenant_id == DEFAULT_TENANT).first()
|
||||
if not existing:
|
||||
session.add(Tenant(
|
||||
tenant_id=DEFAULT_TENANT,
|
||||
name="默认租户",
|
||||
description="系统默认租户"
|
||||
))
|
||||
session.commit()
|
||||
logger.info("默认租户已创建")
|
||||
except Exception:
|
||||
session.rollback()
|
||||
finally:
|
||||
session.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"确保默认租户失败(不影响启动): {e}")
|
||||
|
||||
@contextmanager
|
||||
def get_session(self) -> Generator[Session, None, None]:
|
||||
"""获取数据库会话的上下文管理器"""
|
||||
|
||||
@@ -9,6 +9,35 @@ Base = declarative_base()
|
||||
# 默认租户ID,单租户部署时使用
|
||||
DEFAULT_TENANT = "default"
|
||||
|
||||
|
||||
class Tenant(Base):
|
||||
"""租户模型 — 管理多租户(市场)"""
|
||||
__tablename__ = "tenants"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
tenant_id = Column(String(50), unique=True, nullable=False) # 唯一标识,如 market_a
|
||||
name = Column(String(100), nullable=False) # 显示名称,如 "市场A"
|
||||
description = Column(Text, nullable=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
# 租户级配置:飞书 app 凭证、LLM 参数等(JSON 格式)
|
||||
config = Column(Text, nullable=True)
|
||||
|
||||
def to_dict(self):
|
||||
import json
|
||||
return {
|
||||
'id': self.id,
|
||||
'tenant_id': self.tenant_id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'is_active': self.is_active,
|
||||
'config': json.loads(self.config) if self.config else {},
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
class WorkOrder(Base):
|
||||
"""工单模型"""
|
||||
__tablename__ = "work_orders"
|
||||
|
||||
@@ -46,16 +46,19 @@ class ConversationHistoryManager:
|
||||
knowledge_used: Optional[List[int]] = None,
|
||||
ip_address: Optional[str] = None,
|
||||
invocation_method: Optional[str] = None,
|
||||
session_id: Optional[str] = None
|
||||
session_id: Optional[str] = None,
|
||||
tenant_id: Optional[str] = None
|
||||
) -> int:
|
||||
"""保存对话记录到数据库和Redis"""
|
||||
conversation_id = 0
|
||||
|
||||
try:
|
||||
from src.core.models import DEFAULT_TENANT
|
||||
# 保存到数据库
|
||||
with db_manager.get_session() as session:
|
||||
conversation = Conversation(
|
||||
session_id=session_id,
|
||||
tenant_id=tenant_id or DEFAULT_TENANT,
|
||||
work_order_id=work_order_id,
|
||||
user_message=user_message,
|
||||
assistant_response=assistant_response,
|
||||
|
||||
@@ -273,13 +273,16 @@ class DialogueManager:
|
||||
work_order_id: Optional[int],
|
||||
user_message: str,
|
||||
assistant_response: str,
|
||||
knowledge_used: str
|
||||
knowledge_used: str,
|
||||
tenant_id: Optional[str] = None
|
||||
) -> int:
|
||||
"""保存对话记录"""
|
||||
try:
|
||||
from src.core.models import DEFAULT_TENANT
|
||||
with db_manager.get_session() as session:
|
||||
conversation = Conversation(
|
||||
work_order_id=work_order_id,
|
||||
tenant_id=tenant_id or DEFAULT_TENANT,
|
||||
user_message=user_message,
|
||||
assistant_response=assistant_response,
|
||||
knowledge_used=knowledge_used,
|
||||
@@ -310,10 +313,12 @@ class DialogueManager:
|
||||
title: str,
|
||||
description: str,
|
||||
category: str,
|
||||
priority: str = "medium"
|
||||
priority: str = "medium",
|
||||
tenant_id: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""创建工单"""
|
||||
try:
|
||||
from src.core.models import DEFAULT_TENANT
|
||||
with db_manager.get_session() as session:
|
||||
work_order = WorkOrder(
|
||||
order_id=f"WO{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
@@ -322,6 +327,7 @@ class DialogueManager:
|
||||
category=category,
|
||||
priority=priority,
|
||||
status="open",
|
||||
tenant_id=tenant_id or DEFAULT_TENANT,
|
||||
created_at=datetime.now()
|
||||
)
|
||||
session.add(work_order)
|
||||
|
||||
@@ -39,13 +39,14 @@ class RealtimeChatManager:
|
||||
self.active_sessions = {} # 存储活跃的对话会话
|
||||
self.message_history = {} # 存储消息历史
|
||||
|
||||
def create_session(self, user_id: str, work_order_id: Optional[int] = None) -> str:
|
||||
def create_session(self, user_id: str, work_order_id: Optional[int] = None, tenant_id: Optional[str] = None) -> str:
|
||||
"""创建新的对话会话"""
|
||||
session_id = f"session_{user_id}_{int(time.time())}"
|
||||
|
||||
session_data = {
|
||||
"user_id": user_id,
|
||||
"work_order_id": work_order_id,
|
||||
"tenant_id": tenant_id,
|
||||
"created_at": datetime.now(),
|
||||
"last_activity": datetime.now(),
|
||||
"message_count": 0,
|
||||
@@ -57,11 +58,14 @@ class RealtimeChatManager:
|
||||
|
||||
# 持久化会话到数据库
|
||||
try:
|
||||
from src.core.models import DEFAULT_TENANT
|
||||
effective_tenant = tenant_id or DEFAULT_TENANT
|
||||
with db_manager.get_session() as db_session:
|
||||
chat_session = ChatSession(
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
work_order_id=work_order_id,
|
||||
tenant_id=effective_tenant,
|
||||
status="active",
|
||||
message_count=0,
|
||||
)
|
||||
@@ -96,8 +100,9 @@ class RealtimeChatManager:
|
||||
# 添加到消息历史
|
||||
self.message_history[session_id].append(user_msg)
|
||||
|
||||
# 搜索相关知识
|
||||
knowledge_results = self._search_knowledge(user_message)
|
||||
# 搜索相关知识(按租户隔离)
|
||||
session_tenant = session.get("tenant_id")
|
||||
knowledge_results = self._search_knowledge(user_message, tenant_id=session_tenant)
|
||||
|
||||
# 识别VIN并查询实时数据,注入上下文
|
||||
vin = self._extract_vin(user_message)
|
||||
@@ -180,10 +185,10 @@ class RealtimeChatManager:
|
||||
logger.error(f"处理消息失败: {e}")
|
||||
return {"error": f"处理消息失败: {str(e)}"}
|
||||
|
||||
def _search_knowledge(self, query: str, top_k: int = 3) -> List[Dict[str, Any]]:
|
||||
def _search_knowledge(self, query: str, top_k: int = 3, tenant_id: str = None) -> List[Dict[str, Any]]:
|
||||
"""搜索相关知识"""
|
||||
try:
|
||||
results = self.knowledge_manager.search_knowledge(query, top_k)
|
||||
results = self.knowledge_manager.search_knowledge(query, top_k, tenant_id=tenant_id)
|
||||
return results
|
||||
except Exception as e:
|
||||
logger.error(f"搜索知识库失败: {e}")
|
||||
@@ -474,8 +479,17 @@ class RealtimeChatManager:
|
||||
# 保存知识库使用记录(不再塞 session_marker)
|
||||
knowledge_data = assistant_msg.knowledge_used or []
|
||||
|
||||
# 获取会话的 tenant_id
|
||||
session_tenant = None
|
||||
if session_id in self.active_sessions:
|
||||
session_tenant = self.active_sessions[session_id].get('tenant_id')
|
||||
if not session_tenant:
|
||||
from src.core.models import DEFAULT_TENANT
|
||||
session_tenant = DEFAULT_TENANT
|
||||
|
||||
conversation = Conversation(
|
||||
session_id=session_id,
|
||||
tenant_id=session_tenant,
|
||||
work_order_id=assistant_msg.work_order_id or user_msg.work_order_id,
|
||||
user_message=user_msg.content or "",
|
||||
assistant_response=assistant_msg.content or "",
|
||||
|
||||
@@ -124,7 +124,7 @@ class WorkOrderSyncService:
|
||||
"""获取映射状态"""
|
||||
return self.field_mapper.get_mapping_status()
|
||||
|
||||
def sync_from_feishu(self, generate_ai_suggestions: bool = True, limit: int = 10) -> Dict[str, Any]:
|
||||
def sync_from_feishu(self, generate_ai_suggestions: bool = True, limit: int = 10, tenant_id: str = None) -> Dict[str, Any]:
|
||||
"""
|
||||
从飞书同步工单数据到本地系统
|
||||
|
||||
@@ -215,6 +215,8 @@ class WorkOrderSyncService:
|
||||
# 创建新记录
|
||||
valid_fields["created_at"] = datetime.now()
|
||||
valid_fields["updated_at"] = datetime.now()
|
||||
if tenant_id:
|
||||
valid_fields["tenant_id"] = tenant_id
|
||||
new_workorder = WorkOrder(**valid_fields)
|
||||
session.add(new_workorder)
|
||||
created_count += 1
|
||||
|
||||
@@ -38,6 +38,7 @@ from src.web.blueprints.vehicle import vehicle_bp
|
||||
from src.web.blueprints.analytics import analytics_bp
|
||||
from src.web.blueprints.test import test_bp
|
||||
from src.web.blueprints.feishu_bot import feishu_bot_bp
|
||||
from src.web.blueprints.tenants import tenants_bp
|
||||
|
||||
|
||||
# 配置日志
|
||||
@@ -126,6 +127,7 @@ app.register_blueprint(vehicle_bp)
|
||||
app.register_blueprint(analytics_bp)
|
||||
app.register_blueprint(test_bp)
|
||||
app.register_blueprint(feishu_bot_bp)
|
||||
app.register_blueprint(tenants_bp)
|
||||
|
||||
|
||||
# 页面路由
|
||||
@@ -180,8 +182,9 @@ def create_chat_session():
|
||||
data = request.get_json()
|
||||
user_id = data.get('user_id', 'anonymous')
|
||||
work_order_id = data.get('work_order_id')
|
||||
tenant_id = data.get('tenant_id')
|
||||
|
||||
session_id = service_manager.get_chat_manager().create_session(user_id, work_order_id)
|
||||
session_id = service_manager.get_chat_manager().create_session(user_id, work_order_id, tenant_id=tenant_id)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
|
||||
@@ -92,9 +92,10 @@ def sync_from_feishu():
|
||||
data = request.get_json() or {}
|
||||
generate_ai = data.get('generate_ai_suggestions', True)
|
||||
limit = data.get('limit', 10)
|
||||
tenant_id = data.get('tenant_id')
|
||||
|
||||
sync_service = get_sync_service()
|
||||
result = sync_service.sync_from_feishu(generate_ai_suggestions=generate_ai, limit=limit)
|
||||
result = sync_service.sync_from_feishu(generate_ai_suggestions=generate_ai, limit=limit, tenant_id=tenant_id)
|
||||
|
||||
if result.get("success"):
|
||||
message = f"同步完成:创建 {result['created_count']} 条,更新 {result['updated_count']} 条"
|
||||
|
||||
132
src/web/blueprints/tenants.py
Normal file
132
src/web/blueprints/tenants.py
Normal file
@@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
租户管理蓝图
|
||||
处理租户 CRUD 的 API 路由
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from flask import Blueprint, request, jsonify
|
||||
from src.core.database import db_manager
|
||||
from src.core.models import Tenant, DEFAULT_TENANT
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
tenants_bp = Blueprint('tenants', __name__, url_prefix='/api/tenants')
|
||||
|
||||
|
||||
def _ensure_default_tenant():
|
||||
"""确保 default 租户存在"""
|
||||
try:
|
||||
with db_manager.get_session() as session:
|
||||
existing = session.query(Tenant).filter(Tenant.tenant_id == DEFAULT_TENANT).first()
|
||||
if not existing:
|
||||
session.add(Tenant(
|
||||
tenant_id=DEFAULT_TENANT,
|
||||
name="默认租户",
|
||||
description="系统默认租户"
|
||||
))
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"确保默认租户失败: {e}")
|
||||
|
||||
|
||||
@tenants_bp.route('', methods=['GET'])
|
||||
def list_tenants():
|
||||
"""获取所有租户列表"""
|
||||
try:
|
||||
with db_manager.get_session() as session:
|
||||
tenants = session.query(Tenant).order_by(Tenant.created_at).all()
|
||||
return jsonify([t.to_dict() for t in tenants])
|
||||
except Exception as e:
|
||||
logger.error(f"获取租户列表失败: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@tenants_bp.route('', methods=['POST'])
|
||||
def create_tenant():
|
||||
"""创建新租户"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data or not data.get('tenant_id') or not data.get('name'):
|
||||
return jsonify({"error": "tenant_id 和 name 为必填项"}), 400
|
||||
|
||||
tenant_id = data['tenant_id'].strip()
|
||||
name = data['name'].strip()
|
||||
|
||||
if not tenant_id or not name:
|
||||
return jsonify({"error": "tenant_id 和 name 不能为空"}), 400
|
||||
|
||||
with db_manager.get_session() as session:
|
||||
existing = session.query(Tenant).filter(Tenant.tenant_id == tenant_id).first()
|
||||
if existing:
|
||||
return jsonify({"error": f"租户 '{tenant_id}' 已存在"}), 409
|
||||
|
||||
tenant = Tenant(
|
||||
tenant_id=tenant_id,
|
||||
name=name,
|
||||
description=data.get('description', ''),
|
||||
config=json.dumps(data.get('config', {}))
|
||||
)
|
||||
session.add(tenant)
|
||||
session.commit()
|
||||
|
||||
# 重新查询以获取完整数据
|
||||
tenant = session.query(Tenant).filter(Tenant.tenant_id == tenant_id).first()
|
||||
return jsonify({"success": True, "tenant": tenant.to_dict()}), 201
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建租户失败: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@tenants_bp.route('/<tenant_id>', methods=['PUT'])
|
||||
def update_tenant(tenant_id):
|
||||
"""更新租户信息(重命名等)"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "请求体不能为空"}), 400
|
||||
|
||||
with db_manager.get_session() as session:
|
||||
tenant = session.query(Tenant).filter(Tenant.tenant_id == tenant_id).first()
|
||||
if not tenant:
|
||||
return jsonify({"error": f"租户 '{tenant_id}' 不存在"}), 404
|
||||
|
||||
if 'name' in data:
|
||||
tenant.name = data['name'].strip()
|
||||
if 'description' in data:
|
||||
tenant.description = data['description']
|
||||
if 'config' in data:
|
||||
tenant.config = json.dumps(data['config'])
|
||||
if 'is_active' in data:
|
||||
tenant.is_active = data['is_active']
|
||||
|
||||
session.commit()
|
||||
tenant = session.query(Tenant).filter(Tenant.tenant_id == tenant_id).first()
|
||||
return jsonify({"success": True, "tenant": tenant.to_dict()})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新租户失败: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@tenants_bp.route('/<tenant_id>', methods=['DELETE'])
|
||||
def delete_tenant(tenant_id):
|
||||
"""删除租户(不允许删除 default)"""
|
||||
try:
|
||||
if tenant_id == DEFAULT_TENANT:
|
||||
return jsonify({"error": "不能删除默认租户"}), 403
|
||||
|
||||
with db_manager.get_session() as session:
|
||||
tenant = session.query(Tenant).filter(Tenant.tenant_id == tenant_id).first()
|
||||
if not tenant:
|
||||
return jsonify({"error": f"租户 '{tenant_id}' 不存在"}), 404
|
||||
|
||||
session.delete(tenant)
|
||||
session.commit()
|
||||
return jsonify({"success": True, "message": f"租户 '{tenant_id}' 已删除"})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"删除租户失败: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
@@ -151,7 +151,8 @@ def create_workorder():
|
||||
title=data['title'],
|
||||
description=data['description'],
|
||||
category=data['category'],
|
||||
priority=data['priority']
|
||||
priority=data['priority'],
|
||||
tenant_id=data.get('tenant_id')
|
||||
)
|
||||
|
||||
# 清除工单相关缓存
|
||||
|
||||
@@ -373,11 +373,30 @@ class TSPDashboard {
|
||||
}
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.populateTenantSelectors();
|
||||
// 优化:并行加载初始数据,提高响应速度
|
||||
this.loadInitialDataAsync();
|
||||
this.startAutoRefresh();
|
||||
this.initCharts();
|
||||
}
|
||||
|
||||
async populateTenantSelectors() {
|
||||
try {
|
||||
const response = await fetch('/api/tenants');
|
||||
const tenants = await response.json();
|
||||
if (!Array.isArray(tenants)) return;
|
||||
|
||||
const selectors = document.querySelectorAll('#chat-tenant-id');
|
||||
selectors.forEach(select => {
|
||||
const currentVal = select.value;
|
||||
select.innerHTML = tenants.map(t =>
|
||||
`<option value="${t.tenant_id}"${t.tenant_id === currentVal ? ' selected' : ''}>${t.name} (${t.tenant_id})</option>`
|
||||
).join('');
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('加载租户列表失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async loadInitialDataAsync() {
|
||||
// 并行加载多个数据源
|
||||
@@ -722,7 +741,7 @@ class TSPDashboard {
|
||||
this.loadWorkOrders();
|
||||
break;
|
||||
case 'conversation-history':
|
||||
this.loadConversationHistory();
|
||||
this.loadConversationTenantList();
|
||||
break;
|
||||
case 'token-monitor':
|
||||
this.loadTokenMonitor();
|
||||
@@ -739,6 +758,9 @@ class TSPDashboard {
|
||||
case 'settings':
|
||||
this.loadSettings();
|
||||
break;
|
||||
case 'tenant-management':
|
||||
this.loadTenantList();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1290,6 +1312,7 @@ class TSPDashboard {
|
||||
try {
|
||||
const userId = document.getElementById('user-id').value;
|
||||
const workOrderId = document.getElementById('work-order-id').value;
|
||||
const tenantId = document.getElementById('chat-tenant-id')?.value || 'default';
|
||||
|
||||
const response = await fetch('/api/chat/session', {
|
||||
method: 'POST',
|
||||
@@ -1298,7 +1321,8 @@ class TSPDashboard {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: userId,
|
||||
work_order_id: workOrderId ? parseInt(workOrderId) : null
|
||||
work_order_id: workOrderId ? parseInt(workOrderId) : null,
|
||||
tenant_id: tenantId
|
||||
})
|
||||
});
|
||||
|
||||
@@ -6858,6 +6882,134 @@ class TSPDashboard {
|
||||
this.showNotification('清空Agent历史失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 租户管理 ====================
|
||||
|
||||
async loadTenantList() {
|
||||
const container = document.getElementById('tenant-list');
|
||||
if (!container) return;
|
||||
container.innerHTML = '<div class="text-center py-4"><i class="fas fa-spinner fa-spin fa-2x"></i></div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/tenants');
|
||||
const tenants = await response.json();
|
||||
|
||||
if (!Array.isArray(tenants) || tenants.length === 0) {
|
||||
container.innerHTML = '<div class="text-center py-4 text-muted">暂无租户,请点击"新建租户"创建</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = tenants.map(t => `
|
||||
<div class="card mb-2">
|
||||
<div class="card-body d-flex justify-content-between align-items-center py-2">
|
||||
<div>
|
||||
<strong>${t.name}</strong>
|
||||
<span class="text-muted ms-2">(${t.tenant_id})</span>
|
||||
${t.description ? `<br><small class="text-muted">${t.description}</small>` : ''}
|
||||
${!t.is_active ? '<span class="badge bg-secondary ms-2">已禁用</span>' : ''}
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary" onclick="dashboard.showEditTenantModal('${t.tenant_id}', '${(t.name || '').replace(/'/g, "\\'")}', '${(t.description || '').replace(/'/g, "\\'")}')">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
${t.tenant_id !== 'default' ? `
|
||||
<button class="btn btn-outline-danger" onclick="dashboard.deleteTenant('${t.tenant_id}')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
console.error('加载租户列表失败:', error);
|
||||
container.innerHTML = '<div class="text-center py-4 text-danger">加载失败</div>';
|
||||
}
|
||||
}
|
||||
|
||||
showCreateTenantModal() {
|
||||
document.getElementById('tenantModalTitle').textContent = '新建租户';
|
||||
document.getElementById('tenant-edit-id').value = '';
|
||||
document.getElementById('tenant-id-input').value = '';
|
||||
document.getElementById('tenant-id-input').disabled = false;
|
||||
document.getElementById('tenant-id-group').style.display = '';
|
||||
document.getElementById('tenant-name-input').value = '';
|
||||
document.getElementById('tenant-desc-input').value = '';
|
||||
new bootstrap.Modal(document.getElementById('tenantModal')).show();
|
||||
}
|
||||
|
||||
showEditTenantModal(tenantId, name, description) {
|
||||
document.getElementById('tenantModalTitle').textContent = '编辑租户';
|
||||
document.getElementById('tenant-edit-id').value = tenantId;
|
||||
document.getElementById('tenant-id-input').value = tenantId;
|
||||
document.getElementById('tenant-id-input').disabled = true;
|
||||
document.getElementById('tenant-name-input').value = name;
|
||||
document.getElementById('tenant-desc-input').value = description;
|
||||
new bootstrap.Modal(document.getElementById('tenantModal')).show();
|
||||
}
|
||||
|
||||
async saveTenant() {
|
||||
const editId = document.getElementById('tenant-edit-id').value;
|
||||
const tenantId = document.getElementById('tenant-id-input').value.trim();
|
||||
const name = document.getElementById('tenant-name-input').value.trim();
|
||||
const description = document.getElementById('tenant-desc-input').value.trim();
|
||||
|
||||
if (!name) {
|
||||
this.showNotification('租户名称不能为空', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (editId) {
|
||||
// 编辑
|
||||
response = await fetch(`/api/tenants/${encodeURIComponent(editId)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, description })
|
||||
});
|
||||
} else {
|
||||
// 新建
|
||||
if (!tenantId) {
|
||||
this.showNotification('租户标识不能为空', 'error');
|
||||
return;
|
||||
}
|
||||
response = await fetch('/api/tenants', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tenant_id: tenantId, name, description })
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('tenantModal')).hide();
|
||||
this.showNotification(editId ? '租户已更新' : '租户已创建', 'success');
|
||||
this.loadTenantList();
|
||||
} else {
|
||||
this.showNotification(data.error || '操作失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存租户失败:', error);
|
||||
this.showNotification('保存租户失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteTenant(tenantId) {
|
||||
if (!confirm(`确定要删除租户 "${tenantId}" 吗?该操作不会删除关联数据。`)) return;
|
||||
try {
|
||||
const response = await fetch(`/api/tenants/${encodeURIComponent(tenantId)}`, { method: 'DELETE' });
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
this.showNotification('租户已删除', 'success');
|
||||
this.loadTenantList();
|
||||
} else {
|
||||
this.showNotification(data.error || '删除失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除租户失败:', error);
|
||||
this.showNotification('删除租户失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 飞书同步管理器
|
||||
|
||||
@@ -439,6 +439,10 @@
|
||||
<i class="fas fa-sliders-h"></i>
|
||||
系统设置
|
||||
</a>
|
||||
<a class="nav-link" href="#tenant-management" data-tab="tenant-management">
|
||||
<i class="fas fa-building"></i>
|
||||
租户管理
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
@@ -568,6 +572,13 @@
|
||||
<h5><i class="fas fa-cog me-2"></i>对话控制</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">租户</label>
|
||||
<select class="form-select" id="chat-tenant-id">
|
||||
<option value="default">默认租户</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">用户ID</label>
|
||||
<input type="text" class="form-control" id="user-id" value="user_001">
|
||||
@@ -2187,6 +2198,76 @@
|
||||
</div>
|
||||
|
||||
<!-- 模态框 -->
|
||||
<!-- 租户管理标签页 -->
|
||||
<div id="tenant-management-tab" class="tab-content" style="display: none;">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5><i class="fas fa-building me-2"></i>租户管理</h5>
|
||||
<button class="btn btn-primary btn-sm" onclick="dashboard.showCreateTenantModal()">
|
||||
<i class="fas fa-plus me-1"></i>新建租户
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="tenant-list">
|
||||
<div class="loading-spinner">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-info-circle me-2"></i>说明</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">租户代表不同的市场或业务单元。每个租户拥有独立的知识库、对话历史和工单数据。</p>
|
||||
<ul class="text-muted small">
|
||||
<li>创建租户后,可在各业务模块中选择租户</li>
|
||||
<li>默认租户不可删除</li>
|
||||
<li>可为每个租户配置独立的飞书应用和机器人</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建/编辑租户模态框 -->
|
||||
<div class="modal fade" id="tenantModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="tenantModalTitle">新建租户</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="tenant-edit-id">
|
||||
<div class="mb-3" id="tenant-id-group">
|
||||
<label class="form-label">租户标识 (tenant_id)</label>
|
||||
<input type="text" class="form-control" id="tenant-id-input" placeholder="如 market_a(创建后不可修改)">
|
||||
<div class="form-text">唯一标识,建议使用英文和下划线</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">租户名称</label>
|
||||
<input type="text" class="form-control" id="tenant-name-input" placeholder="如 市场A">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">描述</label>
|
||||
<textarea class="form-control" id="tenant-desc-input" rows="2" placeholder="可选"></textarea>
|
||||
</div>
|
||||
</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="dashboard.saveTenant()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建工单模态框 -->
|
||||
<div class="modal fade" id="createWorkOrderModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
|
||||
@@ -67,8 +67,9 @@ class WebSocketServer:
|
||||
"""处理创建会话请求"""
|
||||
user_id = data.get("user_id", "anonymous")
|
||||
work_order_id = data.get("work_order_id")
|
||||
tenant_id = data.get("tenant_id")
|
||||
|
||||
session_id = self.chat_manager.create_session(user_id, work_order_id)
|
||||
session_id = self.chat_manager.create_session(user_id, work_order_id, tenant_id=tenant_id)
|
||||
|
||||
response = {
|
||||
"type": "session_created",
|
||||
|
||||
Reference in New Issue
Block a user