2026-04-02 09:33:16 +08:00
|
|
|
|
# -*- 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
|
2026-04-02 09:58:04 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolve_tenant_by_chat_id(chat_id: str) -> str:
|
|
|
|
|
|
"""
|
|
|
|
|
|
根据飞书 chat_id 查找对应的 tenant_id。
|
|
|
|
|
|
遍历所有租户的 config.feishu.chat_groups,匹配则返回该 tenant_id。
|
|
|
|
|
|
未匹配时返回 DEFAULT_TENANT。
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
with db_manager.get_session() as session:
|
|
|
|
|
|
tenants = session.query(Tenant).filter(Tenant.is_active == True).all()
|
|
|
|
|
|
for t in tenants:
|
|
|
|
|
|
if not t.config:
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
cfg = json.loads(t.config)
|
|
|
|
|
|
except (json.JSONDecodeError, TypeError):
|
|
|
|
|
|
continue
|
|
|
|
|
|
feishu_cfg = cfg.get('feishu', {})
|
|
|
|
|
|
chat_groups = feishu_cfg.get('chat_groups', [])
|
|
|
|
|
|
if chat_id in chat_groups:
|
|
|
|
|
|
logger.info(f"飞书群 {chat_id} 匹配到租户 {t.tenant_id}")
|
|
|
|
|
|
return t.tenant_id
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"解析飞书群租户映射失败: {e}")
|
2026-04-02 15:21:00 +08:00
|
|
|
|
logger.warning(f"⚠️ 飞书群 {chat_id} 未绑定任何租户,使用默认租户。请在租户管理页面将此 chat_id 绑定到对应租户。")
|
2026-04-02 09:58:04 +08:00
|
|
|
|
return DEFAULT_TENANT
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_tenant_feishu_config(tenant_id: str) -> dict:
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取租户的飞书配置。
|
|
|
|
|
|
返回 {'app_id': ..., 'app_secret': ..., 'chat_groups': [...]} 或空字典。
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
with db_manager.get_session() as session:
|
|
|
|
|
|
tenant = session.query(Tenant).filter(Tenant.tenant_id == tenant_id).first()
|
|
|
|
|
|
if tenant and tenant.config:
|
|
|
|
|
|
cfg = json.loads(tenant.config)
|
|
|
|
|
|
return cfg.get('feishu', {})
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"获取租户飞书配置失败: {e}")
|
|
|
|
|
|
return {}
|