Merge pull request #920 from wanxunyang/feat/quota-check-decorator

feat(tenant): add public subscription plan list endpoint and enhance plan information
This commit is contained in:
山程漫悟
2026-04-17 11:47:34 +08:00
committed by GitHub
8 changed files with 268 additions and 29 deletions

View File

@@ -1,30 +1,77 @@
""" """
社区版默认免费套餐配置 社区版默认免费套餐配置
当无法从 SaaS 版获取 premium 模块时,使用此配置作为兜底 当无法从 SaaS 版获取 premium 模块时,使用此配置作为兜底
可通过环境变量覆盖配额配置格式QUOTA_<QUOTA_NAME>
例如QUOTA_END_USER_QUOTA=100
""" """
DEFAULT_FREE_PLAN = { import os
"name": "记忆体验版",
"category": "saas_personal",
"tier_level": 0, def _get_quota_from_env():
"version": "1.0", """从环境变量获取配额配置"""
"status": True, quota_keys = [
"price": 0, "workspace_quota",
"billing_cycle": "permanent_free", "skill_quota",
"core_value": "感受永久记忆", "app_quota",
"tech_support": "社群交流", "knowledge_capacity_quota",
"sla_compliance": "", "memory_engine_quota",
"page_customization": "", "end_user_quota",
"theme_color": "#64748B", "ontology_project_quota",
"quotas": { "model_quota",
"workspace_quota": 1, "api_ops_rate_limit",
"skill_quota": 5, ]
"app_quota": 2, quotas = {}
"knowledge_capacity_quota": 0.3, for key in quota_keys:
"memory_engine_quota": 1, env_key = f"QUOTA_{key.upper()}"
"end_user_quota": 1, env_value = os.getenv(env_key)
"ontology_project_quota": 3, if env_value is not None:
"model_quota": 1, try:
"api_ops_rate_limit": 50, quotas[key] = float(env_value) if '.' in env_value else int(env_value)
}, except ValueError:
} pass
return quotas
def _build_default_free_plan():
"""构建默认免费套餐配置"""
base = {
"name": "记忆体验版",
"name_en": "Memory Experience",
"category": "saas_personal",
"tier_level": 0,
"version": "1.0",
"status": True,
"price": 0,
"billing_cycle": "permanent_free",
"core_value": "感受永久记忆",
"core_value_en": "Experience Permanent Memory",
"tech_support": "社群交流",
"tech_support_en": "Community Support",
"sla_compliance": "",
"sla_compliance_en": "None",
"page_customization": "",
"page_customization_en": "None",
"theme_color": "#64748B",
"quotas": {
"workspace_quota": 1,
"skill_quota": 5,
"app_quota": 2,
"knowledge_capacity_quota": 0.3,
"memory_engine_quota": 1,
"end_user_quota": 1,
"ontology_project_quota": 3,
"model_quota": 1,
"api_ops_rate_limit": 50,
},
}
env_quotas = _get_quota_from_env()
if env_quotas:
base["quotas"].update(env_quotas)
return base
DEFAULT_FREE_PLAN = _build_default_free_plan()

View File

@@ -100,5 +100,6 @@ manager_router.include_router(ontology_controller.router)
manager_router.include_router(skill_controller.router) manager_router.include_router(skill_controller.router)
manager_router.include_router(i18n_controller.router) manager_router.include_router(i18n_controller.router)
manager_router.include_router(tenant_subscription_controller.router) manager_router.include_router(tenant_subscription_controller.router)
manager_router.include_router(tenant_subscription_controller.public_router)
__all__ = ["manager_router"] __all__ = ["manager_router"]

View File

@@ -271,6 +271,19 @@ def update_agent_config(
return success(data=app_schema.AgentConfig.model_validate(cfg)) return success(data=app_schema.AgentConfig.model_validate(cfg))
@router.get("/{app_id}/model/parameters/default", summary="获取 Agent 模型参数默认配置")
@cur_workspace_access_guard()
def get_agent_model_parameters(
app_id: uuid.UUID,
db: Session = Depends(get_db),
current_user=Depends(get_current_user),
):
workspace_id = current_user.current_workspace_id
service = AppService(db)
model_parameters = service.get_default_model_parameters(app_id=app_id)
return success(data=model_parameters, msg="获取 Agent 模型参数默认配置")
@router.get("/{app_id}/config", summary="获取 Agent 配置") @router.get("/{app_id}/config", summary="获取 Agent 配置")
@cur_workspace_access_guard() @cur_workspace_access_guard()
def get_agent_config( def get_agent_config(

View File

@@ -10,6 +10,7 @@ from sqlalchemy.orm import Session
from app.core.error_codes import BizCode from app.core.error_codes import BizCode
from app.core.exceptions import BusinessException from app.core.exceptions import BusinessException
from app.core.logging_config import get_business_logger from app.core.logging_config import get_business_logger
from app.core.quota_manager import check_end_user_quota
from app.core.response_utils import success, fail from app.core.response_utils import success, fail
from app.db import get_db, get_db_read from app.db import get_db, get_db_read
from app.dependencies import get_share_user_id, ShareTokenData from app.dependencies import get_share_user_id, ShareTokenData
@@ -308,6 +309,7 @@ def get_conversation(
"/chat", "/chat",
summary="发送消息(支持流式和非流式)" summary="发送消息(支持流式和非流式)"
) )
@check_end_user_quota
async def chat( async def chat(
payload: conversation_schema.ChatRequest, payload: conversation_schema.ChatRequest,
share_data: ShareTokenData = Depends(get_share_user_id), share_data: ShareTokenData = Depends(get_share_user_id),

View File

@@ -2,7 +2,7 @@
租户套餐查询接口(普通用户可访问) 租户套餐查询接口(普通用户可访问)
""" """
import datetime import datetime
from typing import Callable from typing import Callable, Optional
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
@@ -19,6 +19,7 @@ from app.schemas.response_schema import ApiResponse
logger = get_api_logger() logger = get_api_logger()
router = APIRouter(prefix="/tenant", tags=["Tenant"]) router = APIRouter(prefix="/tenant", tags=["Tenant"])
public_router = APIRouter(tags=["Tenant"])
@router.get("/subscription", response_model=ApiResponse, summary="获取当前用户所属租户的套餐信息") @router.get("/subscription", response_model=ApiResponse, summary="获取当前用户所属租户的套餐信息")
@@ -42,7 +43,41 @@ async def get_my_tenant_subscription(
sub = svc.get_subscription(tenant_id) sub = svc.get_subscription(tenant_id)
if not sub: if not sub:
return success(data=None, msg="暂无有效套餐") # 无订阅记录时,兜底返回免费套餐信息
free_plan = svc.plan_repo.get_free_plan()
if not free_plan:
return success(data=None, msg="暂无有效套餐")
return success(data={
"subscription_id": None,
"tenant_id": str(tenant_id),
"package_plan_id": str(free_plan.id),
"package_version": free_plan.version,
"package_plan": {
"id": str(free_plan.id),
"name": free_plan.name,
"name_en": free_plan.name_en,
"version": free_plan.version,
"category": free_plan.category,
"tier_level": free_plan.tier_level,
"price": float(free_plan.price) if free_plan.price is not None else 0.0,
"billing_cycle": free_plan.billing_cycle,
"core_value": free_plan.core_value,
"core_value_en": free_plan.core_value_en,
"tech_support": free_plan.tech_support,
"tech_support_en": free_plan.tech_support_en,
"sla_compliance": free_plan.sla_compliance,
"sla_compliance_en": free_plan.sla_compliance_en,
"page_customization": free_plan.page_customization,
"page_customization_en": free_plan.page_customization_en,
"theme_color": free_plan.theme_color,
},
"started_at": None,
"expired_at": None,
"status": "active",
"quota": free_plan.quotas or {},
"created_at": int(datetime.datetime.utcnow().timestamp() * 1000),
"updated_at": int(datetime.datetime.utcnow().timestamp() * 1000),
}, msg="免费套餐")
return success(data=svc.build_response(sub)) return success(data=svc.build_response(sub))
@@ -62,11 +97,21 @@ async def get_my_tenant_subscription(
"package_plan": { "package_plan": {
"id": None, "id": None,
"name": plan["name"], "name": plan["name"],
"name_en": plan.get("name_en"),
"version": plan["version"], "version": plan["version"],
"category": plan["category"], "category": plan["category"],
"tier_level": plan["tier_level"], "tier_level": plan["tier_level"],
"price": float(plan["price"]), "price": float(plan["price"]),
"billing_cycle": plan["billing_cycle"], "billing_cycle": plan["billing_cycle"],
"core_value": plan.get("core_value"),
"core_value_en": plan.get("core_value_en"),
"tech_support": plan.get("tech_support"),
"tech_support_en": plan.get("tech_support_en"),
"sla_compliance": plan.get("sla_compliance"),
"sla_compliance_en": plan.get("sla_compliance_en"),
"page_customization": plan.get("page_customization"),
"page_customization_en": plan.get("page_customization_en"),
"theme_color": plan.get("theme_color"),
}, },
"started_at": None, "started_at": None,
"expired_at": None, "expired_at": None,
@@ -80,3 +125,49 @@ async def get_my_tenant_subscription(
except Exception as e: except Exception as e:
logger.error(f"获取租户套餐信息失败: {e}", exc_info=True) logger.error(f"获取租户套餐信息失败: {e}", exc_info=True)
return JSONResponse(status_code=500, content=fail(code=500, msg="获取套餐信息失败")) return JSONResponse(status_code=500, content=fail(code=500, msg="获取套餐信息失败"))
@public_router.get("/package-plans", response_model=ApiResponse, summary="获取套餐列表(公开)")
async def list_package_plans_public(
category: Optional[str] = None,
status: Optional[bool] = None,
search: Optional[str] = None,
db: Session = Depends(get_db),
):
"""
公开接口,无需鉴权。
SaaS 版从数据库读取套餐列表;社区版降级返回 default_free_plan.py 中的免费套餐。
"""
try:
from premium.platform_admin.package_plan_service import PackagePlanService
from premium.platform_admin.package_plan_schema import PackagePlanResponse
svc = PackagePlanService(db)
result = svc.get_list(page=1, size=9999, category=category, status=status, search=search)
return success(data=[PackagePlanResponse.model_validate(p).model_dump(mode="json") for p in result["items"]])
except ModuleNotFoundError:
from app.config.default_free_plan import DEFAULT_FREE_PLAN
plan = DEFAULT_FREE_PLAN
return success(data=[{
"id": None,
"name": plan["name"],
"name_en": plan.get("name_en"),
"version": plan["version"],
"category": plan["category"],
"tier_level": plan["tier_level"],
"price": float(plan["price"]),
"billing_cycle": plan["billing_cycle"],
"core_value": plan.get("core_value"),
"core_value_en": plan.get("core_value_en"),
"tech_support": plan.get("tech_support"),
"tech_support_en": plan.get("tech_support_en"),
"sla_compliance": plan.get("sla_compliance"),
"sla_compliance_en": plan.get("sla_compliance_en"),
"page_customization": plan.get("page_customization"),
"page_customization_en": plan.get("page_customization_en"),
"theme_color": plan.get("theme_color"),
"status": plan.get("status", True),
"quota": plan["quotas"],
}])
except Exception as e:
logger.error(f"获取套餐列表失败: {e}", exc_info=True)
return JSONResponse(status_code=500, content=fail(code=500, msg="获取套餐列表失败"))

View File

@@ -55,6 +55,18 @@ def _get_tenant_id_from_kwargs(db: Session, kwargs: dict):
if workspace: if workspace:
return workspace.tenant_id return workspace.tenant_id
share_data = kwargs.get("share_data")
if share_data and hasattr(share_data, 'share_token'):
from app.models.workspace_model import Workspace
from app.models.app_model import App
share_token = share_data.share_token
from app.models.release_share_model import ReleaseShare
share_record = db.query(ReleaseShare).filter(ReleaseShare.share_token == share_token).first()
if share_record:
app = db.query(App).filter(App.id == share_record.app_id, App.is_active.is_(True)).first()
if app:
return app.workspace.tenant_id
return None return None

View File

@@ -1452,6 +1452,32 @@ class AppService:
logger.debug("配置不存在,返回默认模板", extra={"app_id": str(app_id)}) logger.debug("配置不存在,返回默认模板", extra={"app_id": str(app_id)})
return self._create_default_agent_config(app_id) return self._create_default_agent_config(app_id)
def get_default_model_parameters(
self,
*,
app_id: uuid.UUID,
) -> "ModelParameters":
"""获取 Agent 默认模型参数(不修改数据库)
Args:
app_id: 应用ID
Returns:
ModelParameters: 默认模型参数
"""
logger.info("获取 Agent 默认模型参数", extra={"app_id": str(app_id)})
app = self._get_app_or_404(app_id)
if app.type != "agent":
raise BusinessException("只有 Agent 类型应用支持 Agent 配置", BizCode.APP_TYPE_NOT_SUPPORTED)
from app.schemas.app_schema import ModelParameters
default_model_parameters = ModelParameters()
logger.info("获取 Agent 默认模型参数成功", extra={"app_id": str(app_id)})
return default_model_parameters
def _create_default_agent_config(self, app_id: uuid.UUID) -> AgentConfig: def _create_default_agent_config(self, app_id: uuid.UUID) -> AgentConfig:
"""创建默认的 Agent 配置模板(不保存到数据库) """创建默认的 Agent 配置模板(不保存到数据库)

View File

@@ -2,11 +2,14 @@ import uuid
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models.user_model import User from app.models.user_model import User
from app.models.knowledge_model import Knowledge from app.models.knowledge_model import Knowledge
from app.models.workspace_model import Workspace
from app.models.models_model import ModelConfig
from app.schemas.knowledge_schema import KnowledgeCreate, KnowledgeUpdate from app.schemas.knowledge_schema import KnowledgeCreate, KnowledgeUpdate
from app.repositories import knowledge_repository from app.repositories import knowledge_repository
from app.core.logging_config import get_business_logger from app.core.logging_config import get_business_logger
from app.repositories.model_repository import ModelConfigRepository
from app.models.models_model import ModelType
# Obtain a dedicated logger for business logic
business_logger = get_business_logger() business_logger = get_business_logger()
@@ -67,6 +70,50 @@ def create_knowledge(
knowledge.workspace_id = current_user.current_workspace_id knowledge.workspace_id = current_user.current_workspace_id
if knowledge.parent_id is None: if knowledge.parent_id is None:
knowledge.parent_id = knowledge.workspace_id knowledge.parent_id = knowledge.workspace_id
workspace = db.query(Workspace).filter(Workspace.id == knowledge.workspace_id).first()
if not workspace:
raise Exception(f"Workspace {knowledge.workspace_id} not found")
tenant_id = workspace.tenant_id
if not knowledge.embedding_id:
embedding_models = ModelConfigRepository.get_by_type(
db=db, model_types=[ModelType.EMBEDDING], tenant_id=tenant_id, is_active=True
)
if embedding_models:
knowledge.embedding_id = embedding_models[0].id
business_logger.debug(f"Auto-bind embedding model: {embedding_models[0].id}")
if not knowledge.reranker_id:
rerank_models = ModelConfigRepository.get_by_type(
db=db, model_types=[ModelType.RERANK], tenant_id=tenant_id, is_active=True
)
if rerank_models:
knowledge.reranker_id = rerank_models[0].id
business_logger.debug(f"Auto-bind rerank model: {rerank_models[0].id}")
if not knowledge.llm_id:
llm_models = ModelConfigRepository.get_by_type(
db=db, model_types=[ModelType.LLM, ModelType.CHAT], tenant_id=tenant_id, is_active=True
)
if llm_models:
knowledge.llm_id = llm_models[0].id
business_logger.debug(f"Auto-bind llm model: {llm_models[0].id}")
if not knowledge.image2text_id:
image2text_models = db.query(ModelConfig).filter(
ModelConfig.tenant_id == tenant_id,
ModelConfig.type.in_([ModelType.CHAT.value]),
ModelConfig.capability.contains(["vision"]),
ModelConfig.is_active == True,
ModelConfig.is_composite == False
).order_by(ModelConfig.created_at.desc()).all()
if not image2text_models:
raise Exception("租户下没有可用的视觉模型,创建知识库失败")
knowledge.image2text_id = image2text_models[0].id
business_logger.debug(f"Auto-bind image2text model: {image2text_models[0].id}")
business_logger.debug(f"Start creating the knowledge base: {knowledge.name}") business_logger.debug(f"Start creating the knowledge base: {knowledge.name}")
db_knowledge = knowledge_repository.create_knowledge( db_knowledge = knowledge_repository.create_knowledge(
db=db, knowledge=knowledge db=db, knowledge=knowledge