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:
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
|
||||
Reference in New Issue
Block a user