feat(tenant): add public subscription plan list endpoint and enhance plan information

Add a public subscription plan list endpoint that can be accessed without authentication. Enhance the returned subscription plan information fields, including multi-language support and default free plan fallback logic. Additionally, implement automatic model binding for the knowledge base service.
This commit is contained in:
wxy
2026-04-16 17:54:50 +08:00
parent 5ce0bdb0f5
commit 915cb54f21
4 changed files with 148 additions and 4 deletions

View File

@@ -5,6 +5,7 @@
DEFAULT_FREE_PLAN = { DEFAULT_FREE_PLAN = {
"name": "记忆体验版", "name": "记忆体验版",
"name_en": "Memory Experience",
"category": "saas_personal", "category": "saas_personal",
"tier_level": 0, "tier_level": 0,
"version": "1.0", "version": "1.0",
@@ -12,9 +13,13 @@ DEFAULT_FREE_PLAN = {
"price": 0, "price": 0,
"billing_cycle": "permanent_free", "billing_cycle": "permanent_free",
"core_value": "感受永久记忆", "core_value": "感受永久记忆",
"core_value_en": "Experience Permanent Memory",
"tech_support": "社群交流", "tech_support": "社群交流",
"tech_support_en": "Community Support",
"sla_compliance": "", "sla_compliance": "",
"sla_compliance_en": "None",
"page_customization": "", "page_customization": "",
"page_customization_en": "None",
"theme_color": "#64748B", "theme_color": "#64748B",
"quotas": { "quotas": {
"workspace_quota": 1, "workspace_quota": 1,

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

@@ -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

@@ -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()
@@ -60,13 +63,57 @@ def create_knowledge(
db: Session, knowledge: KnowledgeCreate, current_user: User db: Session, knowledge: KnowledgeCreate, current_user: User
) -> Knowledge: ) -> Knowledge:
business_logger.info(f"Create a knowledge base: {knowledge.name}, creator: {current_user.username}") business_logger.info(f"Create a knowledge base: {knowledge.name}, creator: {current_user.username}")
try: try:
knowledge.created_by = current_user.id knowledge.created_by = current_user.id
if knowledge.workspace_id is None: if knowledge.workspace_id is None:
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, ModelType.IMAGE.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