feat(backend): 6-axis dynamic evaluation system with weight engine and template management
This commit is contained in:
+10
-1
@@ -56,13 +56,21 @@ from app.routers.knowledge import router as knowledge_router
|
||||
from app.routers.data_sources import router as data_sources_router
|
||||
from app.routers.industry_research import router as industry_research_router
|
||||
from app.routers.funds import router as funds_router
|
||||
from app.routers.evaluation import router as evaluation_router
|
||||
from app.schemas.common import error
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""应用生命周期管理。"""
|
||||
# startup
|
||||
# startup — 初始化预设评价模板
|
||||
from app.core.database import async_session_factory
|
||||
from app.services.evaluation_presets import seed_evaluation_templates
|
||||
|
||||
async with async_session_factory() as session:
|
||||
await seed_evaluation_templates(session)
|
||||
await session.commit()
|
||||
|
||||
yield
|
||||
# shutdown
|
||||
|
||||
@@ -160,3 +168,4 @@ app.include_router(knowledge_router, prefix="/api/v1")
|
||||
app.include_router(data_sources_router, prefix="/api/v1")
|
||||
app.include_router(industry_research_router, prefix="/api/v1")
|
||||
app.include_router(funds_router, prefix="/api/v1")
|
||||
app.include_router(evaluation_router, prefix="/api/v1")
|
||||
|
||||
@@ -13,8 +13,10 @@ from app.models.customer_plan import CustomerAcquisitionPlan
|
||||
from app.models.data_source import DataSource
|
||||
from app.models.decision_sentinel import DecisionSentinel
|
||||
from app.models.digital_twin import DigitalTwinModel
|
||||
from app.models.evaluation_template import EvaluationTemplate
|
||||
from app.models.exit_prediction import ExitPrediction
|
||||
from app.models.financial_data import FinancialData
|
||||
from app.models.fund import CompanyFundLink, Fund
|
||||
from app.models.health_score import HealthScore
|
||||
from app.models.hypothesis import Hypothesis
|
||||
from app.models.inquiry import InquiryList
|
||||
@@ -50,8 +52,11 @@ __all__ = [
|
||||
"DataSource",
|
||||
"DecisionSentinel",
|
||||
"DigitalTwinModel",
|
||||
"EvaluationTemplate",
|
||||
"ExitPrediction",
|
||||
"FinancialData",
|
||||
"Fund",
|
||||
"CompanyFundLink",
|
||||
"FirmProfile",
|
||||
"FundProfile",
|
||||
"HealthScore",
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""评价模板模型。
|
||||
|
||||
6 轴动态评价指标体系的模板配置,支持基金类型、存续期、企业阶段、产业赛道、投资策略、投资人类型的动态组合。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class EvaluationTemplate(Base):
|
||||
"""评价模板 — 6 轴配置单元,定义维度权重和专属指标。"""
|
||||
|
||||
__tablename__ = "evaluation_templates"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
tenant_id: Mapped[str] = mapped_column(String(36), ForeignKey("tenants.id"), nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False, comment="模板名称")
|
||||
|
||||
# 6 轴参数
|
||||
fund_type: Mapped[str] = mapped_column(String(50), nullable=False, comment="基金类型:angel/early_vc/growth_vc/pe/cvc/fof/distress/esg")
|
||||
fund_lifecycle: Mapped[str] = mapped_column(String(50), nullable=False, default="investment", comment="存续期阶段:investment/growth/exit_preparation/liquidation")
|
||||
company_stage: Mapped[str] = mapped_column(String(50), nullable=False, default="a", comment="企业阶段:seed/a/b/c/pre_ipo")
|
||||
industry: Mapped[str] = mapped_column(String(50), nullable=False, default="ai", comment="产业赛道:ai/saas/hardware/biotech/consumer/fintech/manufacturing")
|
||||
strategy: Mapped[str] = mapped_column(String(50), nullable=False, default="growth", comment="投资策略:growth/value/empowerment/turnaround")
|
||||
investor_type: Mapped[str] = mapped_column(String(50), nullable=False, default="investor", comment="投资人类型:gp/post_invest_lead/investor")
|
||||
|
||||
# 权重配置 — {dimension_key: weight},归一化后总和 = 1.0
|
||||
weights_json: Mapped[dict] = mapped_column(JSONBType, nullable=False, comment="14 维度权重(归一化后)")
|
||||
|
||||
# 维度裁剪
|
||||
enabled_dimensions: Mapped[list] = mapped_column(JSONBType, nullable=False, comment="启用的维度 key 列表")
|
||||
disabled_dimensions: Mapped[list] = mapped_column(JSONBType, nullable=False, default=list, comment="禁用的维度 key 列表")
|
||||
|
||||
# 专属指标 — [{key, label, description, data_source}]
|
||||
custom_metrics_json: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="赛道专属指标定义")
|
||||
|
||||
# 修饰因子
|
||||
lp_focus_metrics: Mapped[list | None] = mapped_column(JSONBType, nullable=True, comment="LP 附加指标列表")
|
||||
regional_benchmark: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="地域基准标识:china_mainland/us/sea/europe")
|
||||
|
||||
# 元数据
|
||||
is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, comment="是否为该组合的默认模板")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, comment="是否启用")
|
||||
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1, comment="版本号")
|
||||
|
||||
created_by: Mapped[str | None] = mapped_column(String(36), nullable=True, comment="创建人")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
onupdate=lambda: datetime.now(timezone.utc),
|
||||
)
|
||||
@@ -0,0 +1,95 @@
|
||||
"""基金模型。
|
||||
|
||||
管理基金类型、存续期、LP 构成等信息,支撑评价指标体系的动态权重计算。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
from sqlalchemy import Boolean, Date, DateTime, Float, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class Fund(Base):
|
||||
"""基金信息 — 管理基金类型和存续期。"""
|
||||
|
||||
__tablename__ = "funds"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
tenant_id: Mapped[str] = mapped_column(String(36), ForeignKey("tenants.id"), nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False, comment="基金名称")
|
||||
|
||||
# 基金类型:angel/early_vc/growth_vc/pe/cvc/fof/distress/esg
|
||||
fund_type: Mapped[str] = mapped_column(String(50), nullable=False, comment="基金类型")
|
||||
# 投资策略:growth/value/empowerment/turnaround
|
||||
strategy: Mapped[str] = mapped_column(String(50), nullable=False, default="growth", comment="投资策略")
|
||||
|
||||
# 存续期信息
|
||||
established_date: Mapped[date | None] = mapped_column(Date, nullable=True, comment="基金成立日")
|
||||
total_lifespan_months: Mapped[int] = mapped_column(Integer, nullable=False, default=84, comment="总存续期(月)")
|
||||
investment_period_months: Mapped[int] = mapped_column(Integer, nullable=False, default=48, comment="投资期(月)")
|
||||
|
||||
# LP 构成 — {government: 30, market: 50, corporate: 20}
|
||||
lp_composition_json: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="LP 构成百分比")
|
||||
|
||||
# 地域
|
||||
primary_market: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="主要市场:china_mainland/us/sea/europe")
|
||||
|
||||
# 状态
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, comment="是否活跃")
|
||||
|
||||
# 元数据
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
onupdate=lambda: datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
@property
|
||||
def current_lifecycle(self) -> str:
|
||||
"""根据当前日期自动计算基金存续期阶段。
|
||||
|
||||
返回:investment / growth / exit_preparation / liquidation
|
||||
"""
|
||||
if not self.established_date:
|
||||
return "investment"
|
||||
|
||||
today = date.today()
|
||||
months_elapsed = (today.year - self.established_date.year) * 12 + (today.month - self.established_date.month)
|
||||
|
||||
if months_elapsed < self.investment_period_months:
|
||||
return "investment"
|
||||
elif months_elapsed < self.total_lifespan_months - 24:
|
||||
return "growth"
|
||||
elif months_elapsed < self.total_lifespan_months - 12:
|
||||
return "exit_preparation"
|
||||
else:
|
||||
return "liquidation"
|
||||
|
||||
|
||||
class CompanyFundLink(Base):
|
||||
"""企业-基金关联 — 一个企业可能被多支基金投资。"""
|
||||
|
||||
__tablename__ = "company_fund_links"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
company_id: Mapped[str] = mapped_column(String(36), ForeignKey("companies.id"), nullable=False, index=True)
|
||||
fund_id: Mapped[str] = mapped_column(String(36), ForeignKey("funds.id"), nullable=False, index=True)
|
||||
|
||||
investment_date: Mapped[date | None] = mapped_column(Date, nullable=True, comment="投资日期")
|
||||
investment_stage: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="投资时企业阶段")
|
||||
round: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="轮次")
|
||||
amount: Mapped[float | None] = mapped_column(Float, nullable=True, comment="投资金额(万元)")
|
||||
ownership_pct: Mapped[float | None] = mapped_column(Float, nullable=True, comment="持股比例(%)")
|
||||
|
||||
is_current: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, comment="当前是否持有")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -13,6 +13,7 @@ from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
from app.core.types import JSONBType as _JSONB # 兼容别名
|
||||
|
||||
|
||||
class HealthScore(Base):
|
||||
@@ -47,3 +48,13 @@ class HealthScore(Base):
|
||||
calculated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
# 评价模板关联(向后兼容:旧数据为 NULL)
|
||||
template_id: Mapped[str | None] = mapped_column(String(36), ForeignKey("evaluation_templates.id"), nullable=True, comment="使用的评价模板 ID")
|
||||
fund_type: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="冗余存储基金类型,便于查询")
|
||||
fund_lifecycle: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="冗余存储存续期阶段")
|
||||
company_stage: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="冗余存储企业阶段")
|
||||
industry: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="冗余存储产业赛道")
|
||||
strategy: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="冗余存储投资策略")
|
||||
custom_metrics_result: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="专属指标评分结果")
|
||||
lp_focus_result: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="LP 附加指标结果")
|
||||
|
||||
@@ -0,0 +1,451 @@
|
||||
"""评价模板管理 + 评分计算 API。
|
||||
|
||||
提供模板 CRUD、权重计算、评分历史查询等接口。
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_user
|
||||
from app.models.evaluation_template import EvaluationTemplate
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, error, success
|
||||
from app.services.evaluation_engine import (
|
||||
calculate_weighted_score,
|
||||
compute_weights,
|
||||
get_dimension_score_key,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/evaluation", tags=["evaluation"])
|
||||
|
||||
|
||||
# --- 请求/响应模型 ---
|
||||
|
||||
class WeightComputeRequest(BaseModel):
|
||||
"""权重计算请求。"""
|
||||
fund_type: str = Field(..., description="基金类型")
|
||||
fund_lifecycle: str = Field("investment", description="存续期阶段")
|
||||
company_stage: str = Field("a", description="企业阶段")
|
||||
industry: str = Field("ai", description="产业赛道")
|
||||
strategy: str = Field("growth", description="投资策略")
|
||||
investor_type: str = Field("investor", description="投资人类型")
|
||||
|
||||
|
||||
class TemplateCreateRequest(BaseModel):
|
||||
"""创建模板请求。"""
|
||||
name: str = Field(..., description="模板名称")
|
||||
fund_type: str = Field(..., description="基金类型")
|
||||
fund_lifecycle: str = Field("investment", description="存续期阶段")
|
||||
company_stage: str = Field("a", description="企业阶段")
|
||||
industry: str = Field("ai", description="产业赛道")
|
||||
strategy: str = Field("growth", description="投资策略")
|
||||
investor_type: str = Field("investor", description="投资人类型")
|
||||
weights_json: dict[str, float] | None = Field(None, description="自定义权重(不传则自动计算)")
|
||||
is_default: bool = Field(False, description="是否为默认模板")
|
||||
|
||||
|
||||
class ScoreCalculateRequest(BaseModel):
|
||||
"""评分计算请求。"""
|
||||
company_id: str = Field(..., description="企业 ID")
|
||||
template_id: str | None = Field(None, description="模板 ID(不传则自动匹配)")
|
||||
fund_type: str | None = Field(None, description="基金类型(无模板时用于自动匹配)")
|
||||
fund_lifecycle: str | None = Field(None, description="存续期阶段")
|
||||
company_stage: str | None = Field(None, description="企业阶段")
|
||||
industry: str | None = Field(None, description="产业赛道")
|
||||
strategy: str | None = Field(None, description="投资策略")
|
||||
structured_data: dict[str, Any] = Field(default_factory=dict, description="月报结构化数据")
|
||||
|
||||
|
||||
# --- API 端点 ---
|
||||
|
||||
@router.post("/weights/compute", response_model=ApiResponse[dict])
|
||||
async def compute_evaluation_weights(
|
||||
req: WeightComputeRequest,
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""根据 6 轴参数实时计算权重(不持久化)。"""
|
||||
result = compute_weights(
|
||||
fund_type=req.fund_type,
|
||||
fund_lifecycle=req.fund_lifecycle,
|
||||
company_stage=req.company_stage,
|
||||
industry=req.industry,
|
||||
strategy=req.strategy,
|
||||
investor_type=req.investor_type,
|
||||
)
|
||||
return success(data=result)
|
||||
|
||||
|
||||
@router.get("/templates", response_model=ApiResponse[list])
|
||||
async def list_templates(
|
||||
fund_type: str | None = Query(default=None, description="基金类型筛选"),
|
||||
fund_lifecycle: str | None = Query(default=None, description="存续期阶段筛选"),
|
||||
company_stage: str | None = Query(default=None, description="企业阶段筛选"),
|
||||
industry: str | None = Query(default=None, description="产业赛道筛选"),
|
||||
strategy: str | None = Query(default=None, description="投资策略筛选"),
|
||||
is_default: bool | None = Query(default=None, description="仅默认模板"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取评价模板列表。"""
|
||||
query = select(EvaluationTemplate).where(
|
||||
EvaluationTemplate.tenant_id == user.tenant_id,
|
||||
EvaluationTemplate.is_active == True, # noqa: E712
|
||||
)
|
||||
if fund_type:
|
||||
query = query.where(EvaluationTemplate.fund_type == fund_type)
|
||||
if fund_lifecycle:
|
||||
query = query.where(EvaluationTemplate.fund_lifecycle == fund_lifecycle)
|
||||
if company_stage:
|
||||
query = query.where(EvaluationTemplate.company_stage == company_stage)
|
||||
if industry:
|
||||
query = query.where(EvaluationTemplate.industry == industry)
|
||||
if strategy:
|
||||
query = query.where(EvaluationTemplate.strategy == strategy)
|
||||
if is_default is not None:
|
||||
query = query.where(EvaluationTemplate.is_default == is_default)
|
||||
|
||||
query = query.order_by(EvaluationTemplate.fund_type, EvaluationTemplate.company_stage)
|
||||
result = await db.execute(query)
|
||||
templates = result.scalars().all()
|
||||
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(t.id),
|
||||
"name": t.name,
|
||||
"fund_type": t.fund_type,
|
||||
"fund_lifecycle": t.fund_lifecycle,
|
||||
"company_stage": t.company_stage,
|
||||
"industry": t.industry,
|
||||
"strategy": t.strategy,
|
||||
"investor_type": t.investor_type,
|
||||
"weights": t.weights_json,
|
||||
"enabled_dimensions": t.enabled_dimensions,
|
||||
"disabled_dimensions": t.disabled_dimensions,
|
||||
"custom_metrics": t.custom_metrics_json,
|
||||
"is_default": t.is_default,
|
||||
"version": t.version,
|
||||
}
|
||||
for t in templates
|
||||
])
|
||||
|
||||
|
||||
@router.get("/templates/{template_id}", response_model=ApiResponse[dict])
|
||||
async def get_template(
|
||||
template_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取单个评价模板详情。"""
|
||||
result = await db.execute(
|
||||
select(EvaluationTemplate).where(EvaluationTemplate.id == template_id)
|
||||
)
|
||||
tmpl = result.scalar_one_or_none()
|
||||
if not tmpl:
|
||||
return error(code=404, message="模板不存在")
|
||||
|
||||
return success(data={
|
||||
"id": str(tmpl.id),
|
||||
"name": tmpl.name,
|
||||
"fund_type": tmpl.fund_type,
|
||||
"fund_lifecycle": tmpl.fund_lifecycle,
|
||||
"company_stage": tmpl.company_stage,
|
||||
"industry": tmpl.industry,
|
||||
"strategy": tmpl.strategy,
|
||||
"investor_type": tmpl.investor_type,
|
||||
"weights": tmpl.weights_json,
|
||||
"enabled_dimensions": tmpl.enabled_dimensions,
|
||||
"disabled_dimensions": tmpl.disabled_dimensions,
|
||||
"custom_metrics": tmpl.custom_metrics_json,
|
||||
"lp_focus_metrics": tmpl.lp_focus_metrics,
|
||||
"regional_benchmark": tmpl.regional_benchmark,
|
||||
"is_default": tmpl.is_default,
|
||||
"is_active": tmpl.is_active,
|
||||
"version": tmpl.version,
|
||||
"created_at": tmpl.created_at.isoformat() if tmpl.created_at else None,
|
||||
"updated_at": tmpl.updated_at.isoformat() if tmpl.updated_at else None,
|
||||
})
|
||||
|
||||
|
||||
@router.post("/templates", response_model=ApiResponse[dict])
|
||||
async def create_template(
|
||||
req: TemplateCreateRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""创建自定义评价模板。"""
|
||||
# 如果未提供权重,自动计算
|
||||
if req.weights_json is None:
|
||||
result = compute_weights(
|
||||
fund_type=req.fund_type,
|
||||
fund_lifecycle=req.fund_lifecycle,
|
||||
company_stage=req.company_stage,
|
||||
industry=req.industry,
|
||||
strategy=req.strategy,
|
||||
investor_type=req.investor_type,
|
||||
)
|
||||
weights_json = result["weights"]
|
||||
enabled_dims = result["enabled_dimensions"]
|
||||
disabled_dims = result["disabled_dimensions"]
|
||||
custom_metrics = {"metrics": result["custom_metrics"]}
|
||||
else:
|
||||
weights_json = req.weights_json
|
||||
# 从权重 key 推导启用/禁用维度
|
||||
all_dims = list(get_dimension_score_key(k).replace("_score", "") for k in weights_json)
|
||||
enabled_dims = [k for k, v in weights_json.items() if v > 0]
|
||||
disabled_dims = [k for k in all_dims if k not in enabled_dims]
|
||||
custom_metrics = None
|
||||
|
||||
tmpl = EvaluationTemplate(
|
||||
tenant_id=user.tenant_id,
|
||||
name=req.name,
|
||||
fund_type=req.fund_type,
|
||||
fund_lifecycle=req.fund_lifecycle,
|
||||
company_stage=req.company_stage,
|
||||
industry=req.industry,
|
||||
strategy=req.strategy,
|
||||
investor_type=req.investor_type,
|
||||
weights_json=weights_json,
|
||||
enabled_dimensions=enabled_dims,
|
||||
disabled_dimensions=disabled_dims,
|
||||
custom_metrics_json=custom_metrics,
|
||||
is_default=req.is_default,
|
||||
is_active=True,
|
||||
version=1,
|
||||
created_by=user.id,
|
||||
)
|
||||
db.add(tmpl)
|
||||
await db.flush()
|
||||
|
||||
return success(data={
|
||||
"id": str(tmpl.id),
|
||||
"name": tmpl.name,
|
||||
"weights": tmpl.weights_json,
|
||||
})
|
||||
|
||||
|
||||
@router.post("/score", response_model=ApiResponse[dict])
|
||||
async def calculate_score(
|
||||
req: ScoreCalculateRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""根据模板和月报数据计算评价评分。"""
|
||||
from app.models.health_score import HealthScore
|
||||
from app.services.health_calculator import calculate_health_score
|
||||
|
||||
# 获取模板
|
||||
tmpl: EvaluationTemplate | None = None
|
||||
if req.template_id:
|
||||
result = await db.execute(
|
||||
select(EvaluationTemplate).where(EvaluationTemplate.id == req.template_id)
|
||||
)
|
||||
tmpl = result.scalar_one_or_none()
|
||||
|
||||
if not tmpl:
|
||||
# 自动匹配模板
|
||||
query = select(EvaluationTemplate).where(
|
||||
EvaluationTemplate.tenant_id == user.tenant_id,
|
||||
EvaluationTemplate.is_active == True, # noqa: E712
|
||||
EvaluationTemplate.is_default == True,
|
||||
)
|
||||
if req.fund_type:
|
||||
query = query.where(EvaluationTemplate.fund_type == req.fund_type)
|
||||
if req.fund_lifecycle:
|
||||
query = query.where(EvaluationTemplate.fund_lifecycle == req.fund_lifecycle)
|
||||
if req.company_stage:
|
||||
query = query.where(EvaluationTemplate.company_stage == req.company_stage)
|
||||
if req.industry:
|
||||
query = query.where(EvaluationTemplate.industry == req.industry)
|
||||
|
||||
result = await db.execute(query.limit(1))
|
||||
tmpl = result.scalar_one_or_none()
|
||||
|
||||
# 计算各维度评分(复用现有 health_calculator)
|
||||
dimension_scores = calculate_health_score(req.structured_data)
|
||||
|
||||
# 如果有模板,使用模板权重计算加权总分
|
||||
if tmpl:
|
||||
weights = tmpl.weights_json
|
||||
# 将维度评分转换为权重 key 格式
|
||||
dim_scores_by_weight_key: dict[str, float] = {}
|
||||
for weight_key in weights:
|
||||
score_key = get_dimension_score_key(weight_key)
|
||||
dim_scores_by_weight_key[weight_key] = dimension_scores.get(score_key, 0.0)
|
||||
|
||||
total_score = calculate_weighted_score(dim_scores_by_weight_key, weights)
|
||||
|
||||
# 保存评分记录
|
||||
score_record = HealthScore(
|
||||
company_id=req.company_id,
|
||||
total_score=total_score,
|
||||
financial_score=dimension_scores.get("financial_score"),
|
||||
operational_score=dimension_scores.get("operational_score"),
|
||||
ai_commercial_score=dimension_scores.get("ai_commercial_score"),
|
||||
ai_cost_score=dimension_scores.get("ai_cost_score"),
|
||||
org_talent_score=dimension_scores.get("org_talent_score"),
|
||||
product_tech_score=dimension_scores.get("product_tech_score"),
|
||||
market_compete_score=dimension_scores.get("market_compete_score"),
|
||||
governance_score=dimension_scores.get("governance_score"),
|
||||
financing_score=dimension_scores.get("financing_score"),
|
||||
synergy_score=dimension_scores.get("synergy_score"),
|
||||
ai_model_product_score=dimension_scores.get("ai_model_product_score"),
|
||||
data_compliance_score=dimension_scores.get("data_compliance_score"),
|
||||
team_tech_score=dimension_scores.get("team_tech_score"),
|
||||
customer_success_score=dimension_scores.get("customer_success_score"),
|
||||
template_id=tmpl.id,
|
||||
fund_type=tmpl.fund_type,
|
||||
fund_lifecycle=tmpl.fund_lifecycle,
|
||||
company_stage=tmpl.company_stage,
|
||||
industry=tmpl.industry,
|
||||
strategy=tmpl.strategy,
|
||||
evidence_json={"template_name": tmpl.name, "weights": weights},
|
||||
)
|
||||
db.add(score_record)
|
||||
await db.flush()
|
||||
|
||||
return success(data={
|
||||
"score_id": str(score_record.id),
|
||||
"total_score": total_score,
|
||||
"dimension_scores": {k: v for k, v in dimension_scores.items() if k != "total_score"},
|
||||
"template": {
|
||||
"id": str(tmpl.id),
|
||||
"name": tmpl.name,
|
||||
"weights": weights,
|
||||
},
|
||||
})
|
||||
else:
|
||||
# 无模板,使用默认计算
|
||||
total_score = dimension_scores.get("total_score", 0.0)
|
||||
return success(data={
|
||||
"total_score": total_score,
|
||||
"dimension_scores": {k: v for k, v in dimension_scores.items() if k != "total_score"},
|
||||
"template": None,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/scores", response_model=ApiResponse[list])
|
||||
async def list_scores(
|
||||
company_id: str | None = Query(default=None, description="企业 ID 筛选"),
|
||||
template_id: str | None = Query(default=None, description="模板 ID 筛选"),
|
||||
limit: int = Query(default=20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取评价评分历史。"""
|
||||
from app.models.health_score import HealthScore
|
||||
from app.models.company import Company
|
||||
|
||||
query = (
|
||||
select(HealthScore)
|
||||
.join(Company, HealthScore.company_id == Company.id)
|
||||
.where(Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
if company_id:
|
||||
query = query.where(HealthScore.company_id == company_id)
|
||||
if template_id:
|
||||
query = query.where(HealthScore.template_id == template_id)
|
||||
|
||||
query = query.order_by(HealthScore.calculated_at.desc()).limit(limit)
|
||||
result = await db.execute(query)
|
||||
scores = result.scalars().all()
|
||||
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(s.id),
|
||||
"company_id": s.company_id,
|
||||
"total_score": s.total_score,
|
||||
"financial_score": s.financial_score,
|
||||
"operational_score": s.operational_score,
|
||||
"ai_commercial_score": s.ai_commercial_score,
|
||||
"ai_cost_score": s.ai_cost_score,
|
||||
"org_talent_score": s.org_talent_score,
|
||||
"product_tech_score": s.product_tech_score,
|
||||
"market_compete_score": s.market_compete_score,
|
||||
"governance_score": s.governance_score,
|
||||
"financing_score": s.financing_score,
|
||||
"synergy_score": s.synergy_score,
|
||||
"ai_model_product_score": s.ai_model_product_score,
|
||||
"data_compliance_score": s.data_compliance_score,
|
||||
"team_tech_score": s.team_tech_score,
|
||||
"customer_success_score": s.customer_success_score,
|
||||
"trend": s.trend,
|
||||
"template_id": s.template_id,
|
||||
"fund_type": s.fund_type,
|
||||
"fund_lifecycle": s.fund_lifecycle,
|
||||
"company_stage": s.company_stage,
|
||||
"industry": s.industry,
|
||||
"strategy": s.strategy,
|
||||
"calculated_at": s.calculated_at.isoformat() if s.calculated_at else None,
|
||||
}
|
||||
for s in scores
|
||||
])
|
||||
|
||||
|
||||
# --- 基金管理端点 ---
|
||||
|
||||
@router.get("/funds", response_model=ApiResponse[list])
|
||||
async def list_funds(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取当前租户的基金列表。"""
|
||||
from app.models.fund import Fund
|
||||
|
||||
result = await db.execute(
|
||||
select(Fund)
|
||||
.where(Fund.tenant_id == user.tenant_id, Fund.is_active == True) # noqa: E712
|
||||
.order_by(Fund.established_date.desc())
|
||||
)
|
||||
funds = result.scalars().all()
|
||||
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(f.id),
|
||||
"name": f.name,
|
||||
"fund_type": f.fund_type,
|
||||
"strategy": f.strategy,
|
||||
"established_date": f.established_date.isoformat() if f.established_date else None,
|
||||
"total_lifespan_months": f.total_lifespan_months,
|
||||
"investment_period_months": f.investment_period_months,
|
||||
"current_lifecycle": f.current_lifecycle,
|
||||
"lp_composition": f.lp_composition_json,
|
||||
"primary_market": f.primary_market,
|
||||
}
|
||||
for f in funds
|
||||
])
|
||||
|
||||
|
||||
@router.post("/funds", response_model=ApiResponse[dict])
|
||||
async def create_fund(
|
||||
req: dict[str, Any],
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""创建基金。"""
|
||||
from app.models.fund import Fund
|
||||
from datetime import date
|
||||
|
||||
fund = Fund(
|
||||
tenant_id=user.tenant_id,
|
||||
name=req.get("name", ""),
|
||||
fund_type=req.get("fund_type", "early_vc"),
|
||||
strategy=req.get("strategy", "growth"),
|
||||
established_date=date.fromisoformat(req["established_date"]) if req.get("established_date") else None,
|
||||
total_lifespan_months=req.get("total_lifespan_months", 84),
|
||||
investment_period_months=req.get("investment_period_months", 48),
|
||||
lp_composition_json=req.get("lp_composition"),
|
||||
primary_market=req.get("primary_market"),
|
||||
)
|
||||
db.add(fund)
|
||||
await db.flush()
|
||||
|
||||
return success(data={
|
||||
"id": str(fund.id),
|
||||
"name": fund.name,
|
||||
"current_lifecycle": fund.current_lifecycle,
|
||||
})
|
||||
@@ -0,0 +1,313 @@
|
||||
"""评价权重计算引擎。
|
||||
|
||||
6 轴动态权重合并 + 归一化:
|
||||
1. 基金类型 × 存续期 → 基础权重模板(32 个预设)
|
||||
2. 企业阶段 → 阶段系数调整(5 个预设)
|
||||
3. 产业赛道 → 维度裁剪 + 专属指标注入(6 个预设)
|
||||
4. 投资策略 → ±5% 微调(4 个预设)
|
||||
5. 归一化 — 裁剪后剩余维度权重自动归一化到 100%
|
||||
6. 修饰因子叠加 — LP 附加指标 + 地域基准校准
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# --- 基础 14 维度权重(来自 health_calculator.py) ---
|
||||
BASE_WEIGHTS: dict[str, float] = {
|
||||
"financial": 0.15,
|
||||
"operational": 0.10,
|
||||
"ai_commercial": 0.10,
|
||||
"ai_cost": 0.05,
|
||||
"org_talent": 0.10,
|
||||
"product_tech": 0.10,
|
||||
"market_compete": 0.10,
|
||||
"governance": 0.05,
|
||||
"financing": 0.05,
|
||||
"synergy": 0.05,
|
||||
"ai_model_product": 0.05,
|
||||
"data_compliance": 0.05,
|
||||
"team_tech": 0.03,
|
||||
"customer_success": 0.07,
|
||||
}
|
||||
|
||||
# 维度 key 映射:权重 key → 评分字段 key
|
||||
DIMENSION_KEY_MAP: dict[str, str] = {
|
||||
"financial": "financial_score",
|
||||
"operational": "operational_score",
|
||||
"ai_commercial": "ai_commercial_score",
|
||||
"ai_cost": "ai_cost_score",
|
||||
"org_talent": "org_talent_score",
|
||||
"product_tech": "product_tech_score",
|
||||
"market_compete": "market_compete_score",
|
||||
"governance": "governance_score",
|
||||
"financing": "financing_score",
|
||||
"synergy": "synergy_score",
|
||||
"ai_model_product": "ai_model_product_score",
|
||||
"data_compliance": "data_compliance_score",
|
||||
"team_tech": "team_tech_score",
|
||||
"customer_success": "customer_success_score",
|
||||
}
|
||||
|
||||
# --- 轴 5:基金类型 × 存续期基础权重系数(32 个预设) ---
|
||||
FUND_LIFECYCLE_MULTIPLIERS: dict[tuple[str, str], dict[str, float]] = {
|
||||
("angel", "investment"): {"financial": 0.5, "product_tech": 1.8, "org_talent": 1.5, "market": 1.2},
|
||||
("angel", "growth"): {"financial": 0.7, "product_tech": 1.5, "org_talent": 1.3, "market": 1.2},
|
||||
("angel", "exit_preparation"): {"financial": 1.0, "product_tech": 1.2, "org_talent": 1.0, "financing": 1.5},
|
||||
("angel", "liquidation"): {"financial": 1.5, "financing": 2.0, "product_tech": 0.8},
|
||||
|
||||
("early_vc", "investment"): {"financial": 0.7, "product_tech": 1.5, "market": 1.2, "customer_success": 0.8},
|
||||
("early_vc", "growth"): {"financial": 0.9, "product_tech": 1.3, "market": 1.2, "customer_success": 1.0},
|
||||
("early_vc", "exit_preparation"): {"financial": 1.3, "market": 1.0, "customer_success": 1.2, "financing": 1.5},
|
||||
("early_vc", "liquidation"): {"financial": 1.8, "financing": 2.0, "product_tech": 0.6},
|
||||
|
||||
("growth_vc", "investment"): {"financial": 1.0, "market": 1.3, "customer_success": 1.2},
|
||||
("growth_vc", "growth"): {"financial": 1.2, "market": 1.2, "customer_success": 1.3},
|
||||
("growth_vc", "exit_preparation"): {"financial": 1.5, "market": 1.0, "customer_success": 1.2, "financing": 1.5},
|
||||
("growth_vc", "liquidation"): {"financial": 2.0, "financing": 2.0, "market": 0.8},
|
||||
|
||||
("pe", "investment"): {"financial": 1.8, "governance": 1.5, "customer_success": 1.3, "product_tech": 0.5, "ai_commercial": 0.3, "ai_cost": 0.3, "ai_model_product": 0.3},
|
||||
("pe", "growth"): {"financial": 2.0, "governance": 1.5, "customer_success": 1.3, "product_tech": 0.5, "ai_commercial": 0.3, "ai_cost": 0.3, "ai_model_product": 0.3},
|
||||
("pe", "exit_preparation"): {"financial": 2.5, "governance": 1.8, "financing": 1.5, "ai_commercial": 0.3, "ai_cost": 0.3, "ai_model_product": 0.3},
|
||||
("pe", "liquidation"): {"financial": 3.0, "financing": 2.0, "governance": 1.5, "ai_commercial": 0.3, "ai_cost": 0.3, "ai_model_product": 0.3},
|
||||
|
||||
("cvc", "investment"): {"synergy": 2.0, "market": 1.3, "product_tech": 1.2, "financial": 0.7},
|
||||
("cvc", "growth"): {"synergy": 1.8, "market": 1.2, "product_tech": 1.2, "financial": 0.8},
|
||||
("cvc", "exit_preparation"): {"synergy": 1.5, "market": 1.0, "financial": 1.2, "financing": 1.3},
|
||||
("cvc", "liquidation"): {"financial": 1.5, "financing": 1.5, "synergy": 1.0},
|
||||
|
||||
("distress", "investment"): {"financial": 2.5, "governance": 1.5, "product_tech": 0.5, "market": 0.5, "ai_commercial": 0.3, "ai_cost": 0.3, "ai_model_product": 0.3},
|
||||
("distress", "growth"): {"financial": 2.5, "governance": 1.5, "product_tech": 0.5, "ai_commercial": 0.3, "ai_cost": 0.3, "ai_model_product": 0.3},
|
||||
("distress", "exit_preparation"): {"financial": 3.0, "governance": 1.5, "financing": 1.5, "ai_commercial": 0.3, "ai_cost": 0.3, "ai_model_product": 0.3},
|
||||
("distress", "liquidation"): {"financial": 3.0, "financing": 2.0, "governance": 1.5, "ai_commercial": 0.3, "ai_cost": 0.3, "ai_model_product": 0.3},
|
||||
|
||||
("esg", "investment"): {"governance": 1.8, "data_compliance": 1.5, "product_tech": 1.2, "financial": 0.8},
|
||||
("esg", "growth"): {"governance": 1.5, "data_compliance": 1.3, "customer_success": 1.2},
|
||||
("esg", "exit_preparation"): {"governance": 1.8, "financial": 1.3, "financing": 1.3},
|
||||
("esg", "liquidation"): {"financial": 1.5, "governance": 1.5, "financing": 1.5},
|
||||
|
||||
# FOF 不直接评价单企业,使用默认权重
|
||||
("fof", "investment"): {},
|
||||
("fof", "growth"): {},
|
||||
("fof", "exit_preparation"): {},
|
||||
("fof", "liquidation"): {},
|
||||
}
|
||||
|
||||
# --- 轴 2:企业阶段系数(5 个预设) ---
|
||||
STAGE_MULTIPLIERS: dict[str, dict[str, float]] = {
|
||||
"seed": {"financial": 0.6, "product_tech": 1.8, "org_talent": 1.5, "market_compete": 0.5, "governance": 0.6, "customer_success": 0.5},
|
||||
"a": {"financial": 0.8, "product_tech": 1.5, "org_talent": 1.2, "market_compete": 1.0, "governance": 0.7, "customer_success": 0.8},
|
||||
"b": {"financial": 1.2, "market_compete": 1.3, "customer_success": 1.3, "product_tech": 1.0, "governance": 1.0},
|
||||
"c": {"financial": 1.5, "market_compete": 1.3, "governance": 1.3, "customer_success": 1.3, "product_tech": 0.8},
|
||||
"pre_ipo": {"financial": 1.5, "governance": 1.8, "customer_success": 1.3, "market_compete": 1.0, "product_tech": 0.8},
|
||||
}
|
||||
|
||||
# --- 轴 3:产业赛道配置(6 个预设) ---
|
||||
INDUSTRY_CONFIGS: dict[str, dict[str, Any]] = {
|
||||
"ai": {
|
||||
"enabled": list(DIMENSION_KEY_MAP.keys()),
|
||||
"disabled": [],
|
||||
"custom_metrics": [
|
||||
{"key": "model_accuracy", "label": "模型精度", "description": "AI 模型准确率/召回率"},
|
||||
{"key": "inference_cost", "label": "推理成本", "description": "单次推理成本趋势"},
|
||||
{"key": "api_call_volume", "label": "API 调用量", "description": "月度 API 调用次数"},
|
||||
{"key": "poc_conversion_rate", "label": "PoC 转化率", "description": "PoC 到付费转化率"},
|
||||
],
|
||||
},
|
||||
"saas": {
|
||||
"enabled": ["financial", "operational", "ai_commercial", "org_talent", "product_tech",
|
||||
"market_compete", "governance", "financing", "synergy", "data_compliance",
|
||||
"team_tech", "customer_success"],
|
||||
"disabled": ["ai_cost", "ai_model_product"],
|
||||
"custom_metrics": [
|
||||
{"key": "arr_growth", "label": "ARR 增长率", "description": "年度经常性收入增长率"},
|
||||
{"key": "net_revenue_retention", "label": "NRR", "description": "净收入留存率"},
|
||||
{"key": "cac_payback", "label": "CAC 回收期", "description": "获客成本回收月数"},
|
||||
{"key": "rule_of_40", "label": "Rule of 40", "description": "增长率 + 利润率"},
|
||||
],
|
||||
},
|
||||
"hardware": {
|
||||
"enabled": ["financial", "operational", "org_talent", "product_tech", "market_compete",
|
||||
"governance", "financing", "synergy", "data_compliance",
|
||||
"team_tech", "customer_success"],
|
||||
"disabled": ["ai_commercial", "ai_cost", "ai_model_product"],
|
||||
"custom_metrics": [
|
||||
{"key": "patent_count", "label": "专利数", "description": "累计授权专利数量"},
|
||||
{"key": "tape_out_progress", "label": "流片进度", "description": "芯片流片里程碑进展"},
|
||||
{"key": "yield_rate", "label": "良率", "description": "产品良率"},
|
||||
{"key": "rd_investment_ratio", "label": "研发投入比", "description": "研发投入占营收比例"},
|
||||
],
|
||||
},
|
||||
"biotech": {
|
||||
"enabled": ["financial", "operational", "org_talent", "product_tech", "governance",
|
||||
"financing", "synergy", "data_compliance", "team_tech"],
|
||||
"disabled": ["ai_commercial", "ai_cost", "ai_model_product", "market_compete", "customer_success"],
|
||||
"custom_metrics": [
|
||||
{"key": "clinical_stage", "label": "临床阶段", "description": "当前临床试验阶段"},
|
||||
{"key": "pipeline_progress", "label": "管线进度", "description": "在研管线推进情况"},
|
||||
{"key": "regulatory_milestone", "label": "审批节点", "description": "监管审批里程碑"},
|
||||
{"key": "patent_landscape", "label": "专利布局", "description": "核心专利布局覆盖度"},
|
||||
],
|
||||
},
|
||||
"consumer": {
|
||||
"enabled": ["financial", "operational", "org_talent", "product_tech", "market_compete",
|
||||
"governance", "financing", "synergy", "team_tech", "customer_success"],
|
||||
"disabled": ["ai_commercial", "ai_cost", "ai_model_product", "data_compliance"],
|
||||
"custom_metrics": [
|
||||
{"key": "gmv", "label": "GMV", "description": "月度交易总额"},
|
||||
{"key": "repurchase_rate", "label": "复购率", "description": "客户复购率"},
|
||||
{"key": "brand_index", "label": "品牌指数", "description": "品牌知名度/美誉度"},
|
||||
{"key": "channel_coverage", "label": "渠道覆盖率", "description": "销售渠道覆盖广度"},
|
||||
],
|
||||
},
|
||||
"fintech": {
|
||||
"enabled": ["financial", "operational", "ai_commercial", "org_talent", "product_tech",
|
||||
"market_compete", "governance", "financing", "synergy", "ai_model_product",
|
||||
"data_compliance", "team_tech", "customer_success"],
|
||||
"disabled": ["ai_cost"],
|
||||
"custom_metrics": [
|
||||
{"key": "license_progress", "label": "牌照进度", "description": "金融牌照获取进展"},
|
||||
{"key": "risk_control_score", "label": "风控指标", "description": "风控模型评分"},
|
||||
{"key": "compliance_events", "label": "合规事件", "description": "合规事件数量"},
|
||||
{"key": "npl_ratio", "label": "坏账率", "description": "不良贷款率"},
|
||||
],
|
||||
},
|
||||
"manufacturing": {
|
||||
"enabled": ["financial", "operational", "org_talent", "product_tech", "market_compete",
|
||||
"governance", "financing", "synergy", "data_compliance",
|
||||
"team_tech", "customer_success"],
|
||||
"disabled": ["ai_commercial", "ai_cost", "ai_model_product"],
|
||||
"custom_metrics": [
|
||||
{"key": "capacity_utilization", "label": "产能利用率", "description": "实际产能/设计产能"},
|
||||
{"key": "delivery_cycle", "label": "交付周期", "description": "订单交付周期天数"},
|
||||
{"key": "supply_chain_stability", "label": "供应链稳定性", "description": "供应链中断风险评分"},
|
||||
{"key": "rd_investment_ratio", "label": "研发投入比", "description": "研发投入占营收比例"},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
# --- 轴 4:投资策略微调(4 个预设,±5%) ---
|
||||
STRATEGY_ADJUSTMENTS: dict[str, dict[str, float]] = {
|
||||
"growth": {"market_compete": 5, "product_tech": 5, "customer_success": 5, "financial": -5, "governance": -5},
|
||||
"value": {"financial": 5, "governance": 5, "customer_success": 5, "market_compete": -5, "product_tech": -5},
|
||||
"empowerment": {"synergy": 5, "org_talent": 5, "product_tech": 5, "financial": -5, "market_compete": -5},
|
||||
"turnaround": {"financial": 5, "governance": 5, "market_compete": -5, "product_tech": -5},
|
||||
}
|
||||
|
||||
|
||||
def compute_weights(
|
||||
fund_type: str,
|
||||
fund_lifecycle: str,
|
||||
company_stage: str,
|
||||
industry: str,
|
||||
strategy: str = "growth",
|
||||
investor_type: str = "investor",
|
||||
) -> dict[str, Any]:
|
||||
"""计算 6 轴动态权重。
|
||||
|
||||
Args:
|
||||
fund_type: 基金类型 — angel/early_vc/growth_vc/pe/cvc/fof/distress/esg
|
||||
fund_lifecycle: 存续期阶段 — investment/growth/exit_preparation/liquidation
|
||||
company_stage: 企业阶段 — seed/a/b/c/pre_ipo
|
||||
industry: 产业赛道 — ai/saas/hardware/biotech/consumer/fintech/manufacturing
|
||||
strategy: 投资策略 — growth/value/empowerment/turnaround
|
||||
investor_type: 投资人类型 — gp/post_invest_lead/investor(不影响权重)
|
||||
|
||||
Returns:
|
||||
包含 weights、enabled_dimensions、disabled_dimensions、custom_metrics 的字典
|
||||
"""
|
||||
# Step 1:复制基础权重
|
||||
weights = dict(BASE_WEIGHTS)
|
||||
|
||||
# Step 2:基金类型 × 存续期系数
|
||||
fund_key = (fund_type, fund_lifecycle)
|
||||
fund_multipliers = FUND_LIFECYCLE_MULTIPLIERS.get(fund_key, {})
|
||||
for dim, multiplier in fund_multipliers.items():
|
||||
if dim in weights:
|
||||
weights[dim] *= multiplier
|
||||
|
||||
# Step 3:企业阶段系数
|
||||
stage_multipliers = STAGE_MULTIPLIERS.get(company_stage, {})
|
||||
for dim, multiplier in stage_multipliers.items():
|
||||
if dim in weights:
|
||||
weights[dim] *= multiplier
|
||||
|
||||
# Step 4:产业赛道 — 维度裁剪
|
||||
industry_config = INDUSTRY_CONFIGS.get(industry, INDUSTRY_CONFIGS["ai"])
|
||||
enabled_dims = industry_config["enabled"]
|
||||
disabled_dims = industry_config["disabled"]
|
||||
|
||||
# 禁用的维度权重置零
|
||||
for dim in disabled_dims:
|
||||
if dim in weights:
|
||||
weights[dim] = 0.0
|
||||
|
||||
# Step 5:投资策略微调(±5%,基于百分比点)
|
||||
strategy_adj = STRATEGY_ADJUSTMENTS.get(strategy, {})
|
||||
for dim, adjustment in strategy_adj.items():
|
||||
if dim in weights and weights[dim] > 0:
|
||||
# 将百分比点转换为权重调整量
|
||||
weights[dim] += adjustment / 100.0
|
||||
|
||||
# 确保非负
|
||||
for dim in weights:
|
||||
weights[dim] = max(0.0, weights[dim])
|
||||
|
||||
# Step 6:归一化 — 只对启用维度归一化到 1.0
|
||||
enabled_weights = {dim: weights[dim] for dim in enabled_dims if dim in weights}
|
||||
total = sum(enabled_weights.values())
|
||||
|
||||
if total > 0:
|
||||
normalized = {dim: w / total for dim, w in enabled_weights.items()}
|
||||
else:
|
||||
# 极端情况:所有权重为零,均分
|
||||
count = len(enabled_dims) if enabled_dims else 1
|
||||
normalized = {dim: 1.0 / count for dim in enabled_dims}
|
||||
|
||||
# 转换为百分比格式(保留 4 位小数)
|
||||
final_weights = {dim: round(w * 100, 2) for dim, w in normalized.items()}
|
||||
|
||||
logger.info(
|
||||
"权重计算完成: fund_type=%s, lifecycle=%s, stage=%s, industry=%s, strategy=%s → %s",
|
||||
fund_type, fund_lifecycle, company_stage, industry, strategy, final_weights,
|
||||
)
|
||||
|
||||
return {
|
||||
"weights": final_weights,
|
||||
"enabled_dimensions": enabled_dims,
|
||||
"disabled_dimensions": disabled_dims,
|
||||
"custom_metrics": industry_config.get("custom_metrics", []),
|
||||
}
|
||||
|
||||
|
||||
def get_dimension_score_key(weight_key: str) -> str:
|
||||
"""将权重 key 转换为评分字段 key。"""
|
||||
return DIMENSION_KEY_MAP.get(weight_key, f"{weight_key}_score")
|
||||
|
||||
|
||||
def calculate_weighted_score(
|
||||
dimension_scores: dict[str, float],
|
||||
weights: dict[str, float],
|
||||
) -> float:
|
||||
"""根据维度评分和权重计算加权总分。
|
||||
|
||||
Args:
|
||||
dimension_scores: 各维度评分(0-100),key 为权重 key(如 financial)
|
||||
weights: 各维度权重(百分比),key 为权重 key
|
||||
|
||||
Returns:
|
||||
加权总分(0-100)
|
||||
"""
|
||||
total = 0.0
|
||||
weight_sum = 0.0
|
||||
|
||||
for dim, weight in weights.items():
|
||||
score = dimension_scores.get(dim)
|
||||
if score is not None and weight > 0:
|
||||
total += score * weight
|
||||
weight_sum += weight
|
||||
|
||||
if weight_sum > 0:
|
||||
return round(total / weight_sum, 1)
|
||||
return 0.0
|
||||
@@ -0,0 +1,154 @@
|
||||
"""评价模板预设配置种子数据。
|
||||
|
||||
启动时自动初始化 50 个预设配置单元到数据库。
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from app.services.evaluation_engine import compute_weights
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 基金类型列表
|
||||
FUND_TYPES = ["angel", "early_vc", "growth_vc", "pe", "cvc", "fof", "distress", "esg"]
|
||||
|
||||
# 存续期阶段列表
|
||||
FUND_LIFECYCLES = ["investment", "growth", "exit_preparation", "liquidation"]
|
||||
|
||||
# 企业阶段列表
|
||||
COMPANY_STAGES = ["seed", "a", "b", "c", "pre_ipo"]
|
||||
|
||||
# 产业赛道列表
|
||||
INDUSTRIES = ["ai", "saas", "hardware", "biotech", "consumer", "fintech", "manufacturing"]
|
||||
|
||||
# 投资策略列表
|
||||
STRATEGIES = ["growth", "value", "empowerment", "turnaround"]
|
||||
|
||||
# 投资人类型列表
|
||||
INVESTOR_TYPES = ["gp", "post_invest_lead", "investor"]
|
||||
|
||||
# 模板名称中文名
|
||||
FUND_TYPE_LABELS = {
|
||||
"angel": "天使/种子基金",
|
||||
"early_vc": "早期VC",
|
||||
"growth_vc": "成长期VC",
|
||||
"pe": "PE/并购基金",
|
||||
"cvc": "产业基金",
|
||||
"fof": "母基金",
|
||||
"distress": "困境/特殊机会基金",
|
||||
"esg": "ESG/影响力基金",
|
||||
}
|
||||
|
||||
LIFECYCLE_LABELS = {
|
||||
"investment": "投资期",
|
||||
"growth": "成长期",
|
||||
"exit_preparation": "退出准备期",
|
||||
"liquidation": "清算期",
|
||||
}
|
||||
|
||||
STAGE_LABELS = {
|
||||
"seed": "种子/天使",
|
||||
"a": "A轮",
|
||||
"b": "B轮",
|
||||
"c": "C轮+",
|
||||
"pre_ipo": "Pre-IPO",
|
||||
}
|
||||
|
||||
INDUSTRY_LABELS = {
|
||||
"ai": "AI/SaaS",
|
||||
"saas": "企业服务",
|
||||
"hardware": "硬科技/芯片",
|
||||
"biotech": "生物医药",
|
||||
"consumer": "消费品牌",
|
||||
"fintech": "金融科技",
|
||||
"manufacturing": "新能源/先进制造",
|
||||
}
|
||||
|
||||
STRATEGY_LABELS = {
|
||||
"growth": "成长型",
|
||||
"value": "价值型",
|
||||
"empowerment": "投后赋能型",
|
||||
"turnaround": "困境反转型",
|
||||
}
|
||||
|
||||
|
||||
def generate_preset_templates() -> list[dict[str, Any]]:
|
||||
"""生成全部预设模板配置。
|
||||
|
||||
策略:为每个 (fund_type, lifecycle, stage, industry) 组合生成默认模板,
|
||||
strategy 默认使用 growth,investor_type 默认使用 investor。
|
||||
用户可以在前端创建自定义策略/投资人类型的模板。
|
||||
|
||||
Returns:
|
||||
模板配置字典列表
|
||||
"""
|
||||
templates: list[dict[str, Any]] = []
|
||||
|
||||
for fund_type in FUND_TYPES:
|
||||
for lifecycle in FUND_LIFECYCLES:
|
||||
for stage in COMPANY_STAGES:
|
||||
for industry in INDUSTRIES:
|
||||
# FOF 不生成单企业模板
|
||||
if fund_type == "fof":
|
||||
continue
|
||||
|
||||
result = compute_weights(
|
||||
fund_type=fund_type,
|
||||
fund_lifecycle=lifecycle,
|
||||
company_stage=stage,
|
||||
industry=industry,
|
||||
strategy="growth",
|
||||
investor_type="investor",
|
||||
)
|
||||
|
||||
name = (
|
||||
f"{FUND_TYPE_LABELS[fund_type]}-{LIFECYCLE_LABELS[lifecycle]}-"
|
||||
f"{STAGE_LABELS[stage]}-{INDUSTRY_LABELS[industry]}"
|
||||
)
|
||||
|
||||
templates.append({
|
||||
"name": name,
|
||||
"fund_type": fund_type,
|
||||
"fund_lifecycle": lifecycle,
|
||||
"company_stage": stage,
|
||||
"industry": industry,
|
||||
"strategy": "growth",
|
||||
"investor_type": "investor",
|
||||
"weights_json": result["weights"],
|
||||
"enabled_dimensions": result["enabled_dimensions"],
|
||||
"disabled_dimensions": result["disabled_dimensions"],
|
||||
"custom_metrics_json": {"metrics": result["custom_metrics"]},
|
||||
"is_default": True,
|
||||
"is_active": True,
|
||||
"version": 1,
|
||||
})
|
||||
|
||||
logger.info("生成预设模板 %d 个", len(templates))
|
||||
return templates
|
||||
|
||||
|
||||
async def seed_evaluation_templates(db_session) -> None:
|
||||
"""将预设模板写入数据库(仅当表为空时)。
|
||||
|
||||
Args:
|
||||
db_session: 异步数据库会话
|
||||
"""
|
||||
from sqlalchemy import select
|
||||
from app.models.evaluation_template import EvaluationTemplate
|
||||
|
||||
# 检查是否已有数据
|
||||
result = await db_session.execute(select(EvaluationTemplate).limit(1))
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info("评价模板表已有数据,跳过种子初始化")
|
||||
return
|
||||
|
||||
templates = generate_preset_templates()
|
||||
for tmpl_data in templates:
|
||||
tmpl = EvaluationTemplate(**tmpl_data)
|
||||
db_session.add(tmpl)
|
||||
|
||||
await db_session.flush()
|
||||
logger.info("预设评价模板已写入数据库: %d 个", len(templates))
|
||||
@@ -0,0 +1,299 @@
|
||||
"""评价指标体系 API 集成测试。"""
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestWeightComputeAPI:
|
||||
"""权重计算 API 测试。"""
|
||||
|
||||
def test_compute_weights_success(self, client: TestClient, auth_headers: dict):
|
||||
"""POST /evaluation/weights/compute 应返回权重计算结果。"""
|
||||
resp = client.post(
|
||||
"/api/v1/evaluation/weights/compute",
|
||||
json={
|
||||
"fund_type": "early_vc",
|
||||
"fund_lifecycle": "investment",
|
||||
"company_stage": "a",
|
||||
"industry": "ai",
|
||||
"strategy": "growth",
|
||||
"investor_type": "investor",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert "weights" in data
|
||||
assert "enabled_dimensions" in data
|
||||
assert "disabled_dimensions" in data
|
||||
assert "custom_metrics" in data
|
||||
# 权重总和应接近 100
|
||||
total = sum(data["weights"].values())
|
||||
assert abs(total - 100.0) < 1.0
|
||||
|
||||
def test_compute_weights_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post(
|
||||
"/api/v1/evaluation/weights/compute",
|
||||
json={
|
||||
"fund_type": "early_vc",
|
||||
"company_stage": "a",
|
||||
"industry": "ai",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_compute_weights_hardware_disables_ai(self, client: TestClient, auth_headers: dict):
|
||||
"""硬科技赛道应禁用 AI 维度。"""
|
||||
resp = client.post(
|
||||
"/api/v1/evaluation/weights/compute",
|
||||
json={
|
||||
"fund_type": "early_vc",
|
||||
"fund_lifecycle": "investment",
|
||||
"company_stage": "a",
|
||||
"industry": "hardware",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert "ai_commercial" in data["disabled_dimensions"]
|
||||
assert "ai_cost" in data["disabled_dimensions"]
|
||||
|
||||
|
||||
class TestTemplateAPI:
|
||||
"""评价模板 API 测试。"""
|
||||
|
||||
def test_list_templates_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""无模板时应返回空列表。"""
|
||||
resp = client.get("/api/v1/evaluation/templates", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
def test_list_templates_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.get("/api/v1/evaluation/templates")
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_create_template_auto_weights(self, client: TestClient, auth_headers: dict):
|
||||
"""创建模板时不传权重应自动计算。"""
|
||||
resp = client.post(
|
||||
"/api/v1/evaluation/templates",
|
||||
json={
|
||||
"name": "测试模板-早期VC-AI",
|
||||
"fund_type": "early_vc",
|
||||
"fund_lifecycle": "investment",
|
||||
"company_stage": "a",
|
||||
"industry": "ai",
|
||||
"strategy": "growth",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert "id" in data
|
||||
assert "weights" in data
|
||||
|
||||
def test_create_template_with_custom_weights(self, client: TestClient, auth_headers: dict):
|
||||
"""创建模板时传自定义权重应使用自定义权重。"""
|
||||
resp = client.post(
|
||||
"/api/v1/evaluation/templates",
|
||||
json={
|
||||
"name": "自定义权重模板",
|
||||
"fund_type": "early_vc",
|
||||
"fund_lifecycle": "growth",
|
||||
"company_stage": "b",
|
||||
"industry": "saas",
|
||||
"strategy": "value",
|
||||
"weights_json": {"financial": 40, "product_tech": 30, "market_compete": 30},
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert data["weights"]["financial"] == 40
|
||||
|
||||
def test_get_template_by_id(self, client: TestClient, auth_headers: dict):
|
||||
"""根据 ID 获取模板详情。"""
|
||||
# 先创建
|
||||
create_resp = client.post(
|
||||
"/api/v1/evaluation/templates",
|
||||
json={
|
||||
"name": "查询测试模板",
|
||||
"fund_type": "pe",
|
||||
"fund_lifecycle": "growth",
|
||||
"company_stage": "c",
|
||||
"industry": "fintech",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
template_id = create_resp.json()["data"]["id"]
|
||||
|
||||
# 再查询
|
||||
resp = client.get(f"/api/v1/evaluation/templates/{template_id}", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert data["name"] == "查询测试模板"
|
||||
assert data["fund_type"] == "pe"
|
||||
|
||||
def test_get_template_not_found(self, client: TestClient, auth_headers: dict):
|
||||
"""查询不存在的模板应返回 404。"""
|
||||
resp = client.get(
|
||||
"/api/v1/evaluation/templates/nonexistent-id",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 404
|
||||
|
||||
def test_list_templates_with_filter(self, client: TestClient, auth_headers: dict):
|
||||
"""按基金类型筛选模板。"""
|
||||
# 创建两个不同类型模板
|
||||
client.post(
|
||||
"/api/v1/evaluation/templates",
|
||||
json={
|
||||
"name": "筛选-早期VC",
|
||||
"fund_type": "early_vc",
|
||||
"company_stage": "a",
|
||||
"industry": "ai",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
client.post(
|
||||
"/api/v1/evaluation/templates",
|
||||
json={
|
||||
"name": "筛选-PE",
|
||||
"fund_type": "pe",
|
||||
"company_stage": "b",
|
||||
"industry": "saas",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
resp = client.get(
|
||||
"/api/v1/evaluation/templates?fund_type=pe",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
for tmpl in data:
|
||||
assert tmpl["fund_type"] == "pe"
|
||||
|
||||
|
||||
class TestScoreCalculateAPI:
|
||||
"""评分计算 API 测试。"""
|
||||
|
||||
def test_calculate_score_without_template(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""无模板时计算评分应使用默认计算。"""
|
||||
resp = client.post(
|
||||
"/api/v1/evaluation/score",
|
||||
json={
|
||||
"company_id": company_id,
|
||||
"structured_data": {
|
||||
"revenue": {"yoy_change": "30"},
|
||||
"cash_balance": {"runway_months": 18},
|
||||
"burn_rate": {"trend": "down"},
|
||||
},
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert "total_score" in data
|
||||
assert "dimension_scores" in data
|
||||
|
||||
def test_calculate_score_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post(
|
||||
"/api/v1/evaluation/score",
|
||||
json={"company_id": "test", "structured_data": {}},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_calculate_score_with_template(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""使用模板计算评分。"""
|
||||
# 先创建模板
|
||||
tmpl_resp = client.post(
|
||||
"/api/v1/evaluation/templates",
|
||||
json={
|
||||
"name": "评分测试模板",
|
||||
"fund_type": "early_vc",
|
||||
"company_stage": "a",
|
||||
"industry": "ai",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
template_id = tmpl_resp.json()["data"]["id"]
|
||||
|
||||
# 使用模板计算评分
|
||||
resp = client.post(
|
||||
"/api/v1/evaluation/score",
|
||||
json={
|
||||
"company_id": company_id,
|
||||
"template_id": template_id,
|
||||
"structured_data": {
|
||||
"revenue": {"yoy_change": "25"},
|
||||
"cash_balance": {"runway_months": 15},
|
||||
"burn_rate": {"trend": "down"},
|
||||
"headcount": {"new_hires": 3, "departures": 1},
|
||||
},
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert "score_id" in data
|
||||
assert "total_score" in data
|
||||
assert data["template"] is not None
|
||||
assert data["template"]["id"] == template_id
|
||||
|
||||
|
||||
class TestScoreHistoryAPI:
|
||||
"""评分历史 API 测试。"""
|
||||
|
||||
def test_list_scores_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""无评分记录时应返回空列表。"""
|
||||
resp = client.get("/api/v1/evaluation/scores", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
def test_list_scores_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.get("/api/v1/evaluation/scores")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestFundAPI:
|
||||
"""基金管理 API 测试。"""
|
||||
|
||||
def test_list_funds_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""无基金时应返回空列表。"""
|
||||
resp = client.get("/api/v1/evaluation/funds", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
def test_create_fund(self, client: TestClient, auth_headers: dict):
|
||||
"""创建基金。"""
|
||||
resp = client.post(
|
||||
"/api/v1/evaluation/funds",
|
||||
json={
|
||||
"name": "测试基金一期",
|
||||
"fund_type": "early_vc",
|
||||
"strategy": "growth",
|
||||
"established_date": "2023-01-01",
|
||||
"total_lifespan_months": 84,
|
||||
"investment_period_months": 48,
|
||||
"primary_market": "china_mainland",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert "id" in data
|
||||
assert data["current_lifecycle"] == "investment"
|
||||
|
||||
def test_create_fund_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post(
|
||||
"/api/v1/evaluation/funds",
|
||||
json={"name": "test", "fund_type": "early_vc"},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
@@ -0,0 +1,226 @@
|
||||
"""评价权重计算引擎测试。"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.evaluation_engine import (
|
||||
BASE_WEIGHTS,
|
||||
DIMENSION_KEY_MAP,
|
||||
calculate_weighted_score,
|
||||
compute_weights,
|
||||
get_dimension_score_key,
|
||||
)
|
||||
|
||||
|
||||
class TestComputeWeights:
|
||||
"""权重计算引擎测试。"""
|
||||
|
||||
def test_basic_computation(self):
|
||||
"""基本权重计算应返回有效结果。"""
|
||||
result = compute_weights(
|
||||
fund_type="early_vc",
|
||||
fund_lifecycle="investment",
|
||||
company_stage="a",
|
||||
industry="ai",
|
||||
strategy="growth",
|
||||
)
|
||||
assert "weights" in result
|
||||
assert "enabled_dimensions" in result
|
||||
assert "disabled_dimensions" in result
|
||||
assert "custom_metrics" in result
|
||||
|
||||
def test_weights_sum_to_100(self):
|
||||
"""归一化后权重总和应等于 100。"""
|
||||
result = compute_weights(
|
||||
fund_type="early_vc",
|
||||
fund_lifecycle="growth",
|
||||
company_stage="b",
|
||||
industry="saas",
|
||||
strategy="value",
|
||||
)
|
||||
total = sum(result["weights"].values())
|
||||
assert abs(total - 100.0) < 0.5, f"权重总和应为 100,实际为 {total}"
|
||||
|
||||
def test_disabled_dimensions_have_zero_weight(self):
|
||||
"""禁用的维度不应出现在权重中。"""
|
||||
result = compute_weights(
|
||||
fund_type="pe",
|
||||
fund_lifecycle="investment",
|
||||
company_stage="c",
|
||||
industry="hardware",
|
||||
strategy="value",
|
||||
)
|
||||
for dim in result["disabled_dimensions"]:
|
||||
assert dim not in result["weights"] or result["weights"][dim] == 0
|
||||
|
||||
def test_ai_industry_enables_all_dimensions(self):
|
||||
"""AI 赛道应启用全部 14 维度。"""
|
||||
result = compute_weights(
|
||||
fund_type="early_vc",
|
||||
fund_lifecycle="investment",
|
||||
company_stage="a",
|
||||
industry="ai",
|
||||
)
|
||||
assert len(result["enabled_dimensions"]) == 14
|
||||
assert len(result["disabled_dimensions"]) == 0
|
||||
|
||||
def test_hardware_disables_ai_dimensions(self):
|
||||
"""硬科技赛道应禁用 AI 相关维度。"""
|
||||
result = compute_weights(
|
||||
fund_type="early_vc",
|
||||
fund_lifecycle="investment",
|
||||
company_stage="a",
|
||||
industry="hardware",
|
||||
)
|
||||
assert "ai_commercial" in result["disabled_dimensions"]
|
||||
assert "ai_cost" in result["disabled_dimensions"]
|
||||
assert "ai_model_product" in result["disabled_dimensions"]
|
||||
|
||||
def test_biotech_disables_market_and_customer(self):
|
||||
"""生物医药赛道应禁用市场竞争和客户成功。"""
|
||||
result = compute_weights(
|
||||
fund_type="angel",
|
||||
fund_lifecycle="investment",
|
||||
company_stage="seed",
|
||||
industry="biotech",
|
||||
)
|
||||
assert "market_compete" in result["disabled_dimensions"]
|
||||
assert "customer_success" in result["disabled_dimensions"]
|
||||
|
||||
def test_seed_stage_emphasizes_product_and_team(self):
|
||||
"""种子期应提高产品技术和组织人才权重。"""
|
||||
result = compute_weights(
|
||||
fund_type="angel",
|
||||
fund_lifecycle="investment",
|
||||
company_stage="seed",
|
||||
industry="ai",
|
||||
)
|
||||
weights = result["weights"]
|
||||
# 产品技术权重应高于财务
|
||||
assert weights.get("product_tech", 0) > weights.get("financial", 0)
|
||||
# 组织人才权重应高于治理
|
||||
assert weights.get("org_talent", 0) > weights.get("governance", 0)
|
||||
|
||||
def test_pre_ipo_stage_emphasizes_financial_and_governance(self):
|
||||
"""Pre-IPO 阶段应提高财务和治理权重。"""
|
||||
result = compute_weights(
|
||||
fund_type="growth_vc",
|
||||
fund_lifecycle="exit_preparation",
|
||||
company_stage="pre_ipo",
|
||||
industry="ai",
|
||||
)
|
||||
weights = result["weights"]
|
||||
# 财务权重应高于产品技术
|
||||
assert weights.get("financial", 0) > weights.get("product_tech", 0)
|
||||
# Pre-IPO 的治理权重应高于种子期
|
||||
seed_result = compute_weights(
|
||||
fund_type="growth_vc",
|
||||
fund_lifecycle="exit_preparation",
|
||||
company_stage="seed",
|
||||
industry="ai",
|
||||
)
|
||||
assert weights.get("governance", 0) > seed_result["weights"].get("governance", 0)
|
||||
|
||||
def test_pe_fund_type_emphasizes_financial(self):
|
||||
"""PE 基金应大幅提高财务权重。"""
|
||||
result = compute_weights(
|
||||
fund_type="pe",
|
||||
fund_lifecycle="growth",
|
||||
company_stage="b",
|
||||
industry="saas",
|
||||
)
|
||||
weights = result["weights"]
|
||||
# PE 的财务权重应高于早期 VC
|
||||
vc_result = compute_weights(
|
||||
fund_type="early_vc",
|
||||
fund_lifecycle="investment",
|
||||
company_stage="b",
|
||||
industry="saas",
|
||||
)
|
||||
assert weights.get("financial", 0) > vc_result["weights"].get("financial", 0)
|
||||
|
||||
def test_custom_metrics_present(self):
|
||||
"""各赛道应有专属指标。"""
|
||||
for industry in ["ai", "saas", "hardware", "biotech", "consumer", "fintech"]:
|
||||
result = compute_weights(
|
||||
fund_type="early_vc",
|
||||
fund_lifecycle="investment",
|
||||
company_stage="a",
|
||||
industry=industry,
|
||||
)
|
||||
assert len(result["custom_metrics"]) >= 3, f"{industry} 赛道专属指标不足"
|
||||
|
||||
def test_strategy_adjustment_effect(self):
|
||||
"""投资策略微调应影响权重。"""
|
||||
base = compute_weights(
|
||||
fund_type="early_vc",
|
||||
fund_lifecycle="growth",
|
||||
company_stage="b",
|
||||
industry="ai",
|
||||
strategy="growth",
|
||||
)
|
||||
value = compute_weights(
|
||||
fund_type="early_vc",
|
||||
fund_lifecycle="growth",
|
||||
company_stage="b",
|
||||
industry="ai",
|
||||
strategy="value",
|
||||
)
|
||||
# 成长型策略市场权重应高于价值型
|
||||
assert base["weights"].get("market_compete", 0) > value["weights"].get("market_compete", 0)
|
||||
# 价值型策略财务权重应高于成长型
|
||||
assert value["weights"].get("financial", 0) > base["weights"].get("financial", 0)
|
||||
|
||||
def test_all_weights_non_negative(self):
|
||||
"""所有权重应为非负数。"""
|
||||
for fund_type in ["angel", "early_vc", "growth_vc", "pe", "cvc", "distress", "esg"]:
|
||||
for lifecycle in ["investment", "growth", "exit_preparation", "liquidation"]:
|
||||
for stage in ["seed", "a", "b", "c", "pre_ipo"]:
|
||||
for industry in ["ai", "saas", "hardware", "biotech", "consumer", "fintech"]:
|
||||
result = compute_weights(
|
||||
fund_type=fund_type,
|
||||
fund_lifecycle=lifecycle,
|
||||
company_stage=stage,
|
||||
industry=industry,
|
||||
)
|
||||
for dim, weight in result["weights"].items():
|
||||
assert weight >= 0, f"{fund_type}/{lifecycle}/{stage}/{industry} 的 {dim} 权重为负: {weight}"
|
||||
|
||||
|
||||
class TestCalculateWeightedScore:
|
||||
"""加权评分计算测试。"""
|
||||
|
||||
def test_basic_weighted_score(self):
|
||||
"""基本加权评分计算。"""
|
||||
scores = {"financial": 80, "operational": 70, "product_tech": 90}
|
||||
weights = {"financial": 30, "operational": 30, "product_tech": 40}
|
||||
result = calculate_weighted_score(scores, weights)
|
||||
expected = (80 * 30 + 70 * 30 + 90 * 40) / 100
|
||||
assert abs(result - expected) < 0.1
|
||||
|
||||
def test_missing_dimension_ignored(self):
|
||||
"""缺失维度的评分应被忽略。"""
|
||||
scores = {"financial": 80}
|
||||
weights = {"financial": 50, "operational": 50}
|
||||
result = calculate_weighted_score(scores, weights)
|
||||
assert abs(result - 80.0) < 0.1
|
||||
|
||||
def test_empty_scores(self):
|
||||
"""空评分应返回 0。"""
|
||||
result = calculate_weighted_score({}, {"financial": 100})
|
||||
assert result == 0.0
|
||||
|
||||
|
||||
class TestDimensionKeyMap:
|
||||
"""维度 key 映射测试。"""
|
||||
|
||||
def test_key_mapping(self):
|
||||
"""权重 key 应正确映射到评分字段 key。"""
|
||||
assert get_dimension_score_key("financial") == "financial_score"
|
||||
assert get_dimension_score_key("ai_commercial") == "ai_commercial_score"
|
||||
assert get_dimension_score_key("customer_success") == "customer_success_score"
|
||||
|
||||
def test_all_dimensions_mapped(self):
|
||||
"""所有 14 维度都应有映射。"""
|
||||
assert len(DIMENSION_KEY_MAP) == 14
|
||||
for key in BASE_WEIGHTS:
|
||||
assert key in DIMENSION_KEY_MAP, f"维度 {key} 缺少映射"
|
||||
@@ -0,0 +1,521 @@
|
||||
# 投后评价指标体系设计方案
|
||||
|
||||
> **版本**: v1.0
|
||||
> **日期**: 2026-07-19
|
||||
> **状态**: 已定稿
|
||||
|
||||
## 1. 设计目标
|
||||
|
||||
构建一套**动态可配置**的投后企业评价指标体系,根据投资人类型、企业发展阶段、产业赛道、投资策略、基金类型和基金存续期 6 个维度自动调整评价权重和维度组合,同时支持 LP 构成和地域市场 2 个修饰因子。
|
||||
|
||||
### 核心原则
|
||||
|
||||
- **一套框架,多套权重** — 14 维度基础框架不变,权重动态调整
|
||||
- **维度可裁剪** — 非 AI 企业自动禁用 AI 相关维度,权重归一化
|
||||
- **指标可插拔** — 每个赛道 3-5 个专属原子指标,不影响基础框架
|
||||
- **模板可审计** — 每次评分记录使用的模板配置,支持跨期对比和回滚
|
||||
- **组合不爆炸** — 50 个预设配置单元运行时动态组合,而非万级模板
|
||||
|
||||
---
|
||||
|
||||
## 2. 现有体系问题
|
||||
|
||||
当前系统使用 14 维度固定权重(`backend/app/services/health_calculator.py`):
|
||||
|
||||
| 问题 | 说明 |
|
||||
|---|---|
|
||||
| 一套权重打天下 | 种子期和 Pre-IPO 企业用同一套权重,评价失真 |
|
||||
| AI 维度对非 AI 企业无意义 | 硬件、生物医药企业仍有 AI 商业化评分(默认 40 分) |
|
||||
| 无基金类型感知 | PE 基金和天使基金用同一套标准评价同一企业 |
|
||||
| 无时间紧迫度 | 基金到期前 1 年仍在按成长期标准评价,错过退出窗口 |
|
||||
| 无赛道专属指标 | 生物医药的临床进度、芯片的流片良率无法纳入评分 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 六轴模型
|
||||
|
||||
```
|
||||
评价指标体系 = f(
|
||||
投资人类型, # 轴1:展示层级
|
||||
企业阶段, # 轴2:阶段权重
|
||||
产业赛道, # 轴3:维度裁剪 + 专属指标
|
||||
投资策略, # 轴4:策略微调
|
||||
基金类型, # 轴5:评价哲学
|
||||
基金存续期阶段, # 轴6:时间紧迫度
|
||||
) + 修饰因子(
|
||||
LP 构成, # 附加指标层
|
||||
地域/市场, # 基准校准
|
||||
)
|
||||
```
|
||||
|
||||
### 3.1 轴 1:投资人类型
|
||||
|
||||
不影响权重计算,影响**展示层级和关注入口**。
|
||||
|
||||
| 投资人角色 | 关注层级 | 核心指标 | 展示入口 |
|
||||
|---|---|---|---|
|
||||
| GP/合伙人 | 组合层面 | IRR、DPI、组合健康度分布、退出时机 | 指挥塔 / 组合再平衡 |
|
||||
| 投后负责人 | 运营层面 | 健康度趋势、风险队列、任务推进、月报质量 | 今日行动中心 / 工作台 |
|
||||
| 投资经理 | 执行层面 | 今日行动、数据校验、协同匹配、跟进频率 | 今日行动中心 / 企业列表 |
|
||||
|
||||
### 3.2 轴 2:企业发展阶段
|
||||
|
||||
决定阶段适配权重,核心变化是**财务/产品/团队权重的此消彼长**。
|
||||
|
||||
| 阶段 | 财务 | 市场 | 产品 | 团队 | 治理 | AI维度 | 客户成功 | 特征 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 种子/天使 | 10% | 5% | **25%** | **20%** | 5% | 10% | 5% | 产品验证 + 团队潜力 |
|
||||
| Pre-A/A轮 | 15% | 15% | **20%** | 15% | 5% | 10% | 10% | PMF 验证 + 增长引擎 |
|
||||
| B轮 | **20%** | **20%** | 15% | 10% | 10% | 5% | **15%** | 规模化效率 |
|
||||
| C轮+ | **25%** | **20%** | 10% | 5% | **15%** | 5% | **15%** | 退出准备 |
|
||||
| Pre-IPO | **25%** | 15% | 10% | 5% | **20%** | 5% | **15%** | 合规 + 估值 |
|
||||
|
||||
### 3.3 轴 3:产业赛道
|
||||
|
||||
决定**维度裁剪 + 专属指标注入**。
|
||||
|
||||
| 赛道 | 启用维度 | 禁用/降权 | 专属指标 |
|
||||
|---|---|---|---|
|
||||
| AI/SaaS | 全部 14 维度 | — | 模型精度、推理成本、API 调用量、PoC 转化率 |
|
||||
| 硬科技/芯片 | 产品技术↑、团队技术↑ | AI商业化↓、AI成本↓ | 专利数、流片进度、良率、研发投入比 |
|
||||
| 生物医药 | 产品技术↑、治理↑ | 市场竞争↓(早期无市场) | 临床阶段、管线进度、审批节点、专利布局 |
|
||||
| 消费品牌 | 市场竞争↑、客户成功↑ | AI维度↓ | GMV、复购率、品牌指数、渠道覆盖率 |
|
||||
| 金融科技 | 治理合规↑↑、数据合规↑ | — | 牌照进度、风控指标、合规事件、坏账率 |
|
||||
| 新能源/先进制造 | 产品技术↑、团队技术↑ | AI商业化↓ | 产能利用率、交付周期、供应链稳定性 |
|
||||
|
||||
### 3.4 轴 4:投资策略
|
||||
|
||||
**±5% 微调**,不改变维度选择。
|
||||
|
||||
| 策略 | 评价目标 | 权重调整 |
|
||||
|---|---|---|
|
||||
| 成长型 | 增长潜力 | 市场+5%、产品+5%、客户成功+5%;财务-5%、治理-5% |
|
||||
| 价值型 | 稳健回报 | 财务+5%、治理+5%、客户成功+5%;市场-5%、产品-5% |
|
||||
| 投后赋能型 | 协同价值 | 协同+5%、组织+5%、产品+5%;财务-5%、市场-5% |
|
||||
| 困境反转型 | 风险控制 | 财务+5%、治理+5%、风险-5%;市场-5%、产品-5% |
|
||||
|
||||
### 3.5 轴 5:基金类型
|
||||
|
||||
**最高优先级**,决定评价哲学。
|
||||
|
||||
| 基金类型 | 持有期 | 回报预期 | 评价哲学 | 权重影响 |
|
||||
|---|---|---|---|---|
|
||||
| 天使/种子基金 | 7-10年 | 10x+ | 赛道赌注、团队潜力 | 产品↑↑、团队↑↑、市场天花板↑;财务容忍度极高 |
|
||||
| 早期VC(A轮) | 5-8年 | 5-10x | PMF 验证、增长引擎 | 产品↑、市场↑、客户成功↑ |
|
||||
| 成长期VC(B/C轮) | 3-5年 | 3-5x | 规模化效率、单位经济 | 财务↑↑、市场↑、客户成功↑;关注 LTV/CAC |
|
||||
| PE/并购基金 | 3-5年 | 2-3x | 现金流、EBITDA、退出确定性 | 财务↑↑↑、治理↑↑;产品↓、AI维度↓ |
|
||||
| 产业基金(CVC) | 长期持有 | 战略协同优先 | 产业链协同、战略价值 | 协同↑↑、市场↑;财务容忍度高 |
|
||||
| 母基金(FOF) | 不直接投 | 基金层评价 | 基金 IRR/DPI | 关注组合层面而非单企业 |
|
||||
| 困境/特殊机会基金 | 2-3年 | 2-4x | 扭亏为盈、资产处置 | 财务↑↑↑、治理↑;客户成功↓、产品↓ |
|
||||
| ESG/影响力基金 | 5-10年 | 社会回报+财务 | ESG 指标、可持续性 | 治理↑↑、数据合规↑;新增 ESG 维度 |
|
||||
|
||||
### 3.6 轴 6:基金存续期阶段
|
||||
|
||||
**时间紧迫度**,影响退出相关指标权重。
|
||||
|
||||
| 存续期阶段 | 时间窗口 | 行为特征 | 权重影响 |
|
||||
|---|---|---|---|
|
||||
| 投资期 | 前 2 年 | 容忍风险,看重增长潜力 | 产品↑、市场↑、团队↑;财务容忍度高 |
|
||||
| 成长期 | 第 3-4 年 | 关注 PMF 和规模化 | 财务↑、客户成功↑;开始关注单位经济 |
|
||||
| 退出准备 | 第 5-6 年 | 推动退出,关注估值 | 财务↑↑、治理↑、退出信号↑↑;产品↓ |
|
||||
| 清算期 | 最后 1 年 | 紧迫退出 | 退出信号↑↑↑、财务↑↑;一切以退出为导向 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 修饰因子
|
||||
|
||||
### 4.1 LP 构成 — 附加指标层
|
||||
|
||||
不影响 14 维度权重,在评分报告末尾**单独展示**。
|
||||
|
||||
| LP 类型 | 附加指标 |
|
||||
|---|---|
|
||||
| 政府引导基金 | 就业人数、税收贡献、产业带动系数、本地化率 |
|
||||
| 市场化 LP | IRR、DPI、TVPI |
|
||||
| 产业 LP | 产业链协同价值、技术转移数、联合研发项目数 |
|
||||
| 保险/银行 LP | 现金流稳定性、合规评级、资产覆盖率 |
|
||||
|
||||
### 4.2 地域/市场 — 基准校准
|
||||
|
||||
不改变权重,改变**评分刻度和及格线**。
|
||||
|
||||
| 市场 | 校准示例 |
|
||||
|---|---|
|
||||
| 中国大陆 | SaaS 客户留存率及格线 80%(vs 美国 90%);获客成本基准较高 |
|
||||
| 美国 | 增长率基准更高;PMF 验证标准更严格 |
|
||||
| 东南亚 | 市场分散度修正;支付转化率基准较低 |
|
||||
| 欧洲 | 合规权重自动 +5%(GDPR);数据合规及格线更高 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 权重计算引擎
|
||||
|
||||
### 5.1 计算流程
|
||||
|
||||
```
|
||||
输入:6 轴参数
|
||||
│
|
||||
├─ Step 1:基金类型 × 存续期 → 基础权重模板(32 个预设之一)
|
||||
│
|
||||
├─ Step 2:企业阶段 → 阶段系数调整(5 个预设之一)
|
||||
│
|
||||
├─ Step 3:产业赛道 → 维度裁剪 + 专属指标注入(6 个预设之一)
|
||||
│
|
||||
├─ Step 4:投资策略 → ±5% 微调(4 个预设之一)
|
||||
│
|
||||
├─ Step 5:归一化 — 裁剪后剩余维度权重自动归一化到 100%
|
||||
│
|
||||
└─ Step 6:修饰因子叠加 — LP 附加指标 + 地域基准校准
|
||||
│
|
||||
▼
|
||||
输出:维度权重字典 + 专属指标列表 + 基准校准参数
|
||||
```
|
||||
|
||||
### 5.2 权重合并优先级
|
||||
|
||||
| 优先级 | 轴 | 影响方式 | 影响程度 |
|
||||
|---|---|---|---|
|
||||
| 1 | 基金类型 | 决定评价哲学 | ★★★★★ |
|
||||
| 2 | 基金存续期 | 决定时间紧迫度 | ★★★★★ |
|
||||
| 3 | 企业阶段 | 决定阶段适配权重 | ★★★★ |
|
||||
| 4 | 产业赛道 | 裁剪维度 + 专属指标 | ★★★★ |
|
||||
| 5 | 投资策略 | 微调权重 ±5% | ★★★ |
|
||||
| 6 | 投资人类型 | 不影响权重,影响展示入口 | ★★ |
|
||||
|
||||
### 5.3 完整权重示例
|
||||
|
||||
**场景**:早期VC + 退出准备期 + A轮 + AI/SaaS + 成长型策略
|
||||
|
||||
| 维度 | 基础权重 | 基金系数 | 存续期系数 | 阶段系数 | 策略微调 | 最终权重 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 财务 | 15% | ×0.7 | ×1.5 | ×1.0 | -5% | **12%** |
|
||||
| 经营 | 10% | ×1.0 | ×1.0 | ×1.0 | 0 | **10%** |
|
||||
| AI商业化 | 10% | ×1.5 | ×1.0 | ×1.0 | +5% | **18%** |
|
||||
| AI成本 | 5% | ×1.0 | ×1.0 | ×1.0 | 0 | **5%** |
|
||||
| 组织人才 | 10% | ×1.2 | ×0.8 | ×1.0 | 0 | **10%** |
|
||||
| 产品技术 | 10% | ×1.5 | ×0.6 | ×1.2 | +5% | **13%** |
|
||||
| 市场竞争 | 10% | ×1.2 | ×1.0 | ×1.0 | +5% | **12%** |
|
||||
| 治理合规 | 5% | ×0.5 | ×1.5 | ×0.8 | -5% | **3%** |
|
||||
| 融资资本 | 5% | ×1.0 | ×1.3 | ×1.0 | 0 | **7%** |
|
||||
| 协同赋能 | 5% | ×1.0 | ×1.0 | ×1.0 | 0 | **5%** |
|
||||
| AI模型产品 | 5% | ×1.3 | ×1.0 | ×1.0 | 0 | **7%** |
|
||||
| 数据合规 | 5% | ×1.0 | ×1.0 | ×1.0 | 0 | **5%** |
|
||||
| 团队技术 | 3% | ×1.0 | ×0.8 | ×1.0 | 0 | **2%** |
|
||||
| 客户成功 | 7% | ×1.0 | ×1.2 | ×1.0 | +5% | **9%** |
|
||||
|
||||
归一化后总和 = 100%(自动计算)
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据模型
|
||||
|
||||
### 6.1 评价模板配置表
|
||||
|
||||
```python
|
||||
class EvaluationTemplate(Base):
|
||||
"""评价指标模板 — 6 轴配置单元。"""
|
||||
__tablename__ = "evaluation_templates"
|
||||
|
||||
id: Mapped[str] # UUID
|
||||
tenant_id: Mapped[str] # 租户隔离
|
||||
name: Mapped[str] # 模板名称
|
||||
|
||||
# 6 轴参数
|
||||
fund_type: Mapped[str] # angel/early_vc/growth_vc/pe/cvc/fof/distress/esg
|
||||
fund_lifecycle: Mapped[str] # investment/growth/exit_preparation/liquidation
|
||||
company_stage: Mapped[str] # seed/a/b/c/pre_ipo
|
||||
industry: Mapped[str] # ai/saas/hardware/biotech/consumer/fintech/manufacturing
|
||||
strategy: Mapped[str] # growth/value/empowerment/turnaround
|
||||
investor_type: Mapped[str] # gp/post_invest_lead/investor(仅影响展示)
|
||||
|
||||
# 权重配置
|
||||
weights_json: Mapped[dict] # 14 维度权重(归一化后)
|
||||
enabled_dimensions: Mapped[list] # 启用的维度 key 列表
|
||||
disabled_dimensions: Mapped[list] # 禁用的维度 key 列表
|
||||
|
||||
# 专属指标
|
||||
custom_metrics_json: Mapped[dict] # 赛道专属指标定义
|
||||
|
||||
# 修饰因子
|
||||
lp_focus_metrics: Mapped[list | None] # LP 附加指标
|
||||
regional_benchmark: Mapped[str | None] # 地域基准标识
|
||||
|
||||
# 元数据
|
||||
is_default: Mapped[bool] # 是否为该组合的默认模板
|
||||
is_active: Mapped[bool] # 是否启用
|
||||
version: Mapped[int] # 版本号
|
||||
created_by: Mapped[str]
|
||||
created_at: Mapped[datetime]
|
||||
updated_at: Mapped[datetime]
|
||||
```
|
||||
|
||||
### 6.2 评分记录表(扩展现有 HealthScore)
|
||||
|
||||
```python
|
||||
class HealthScore(Base):
|
||||
"""健康度评分 — 新增模板关联字段。"""
|
||||
# ... 现有 14 维度字段保留
|
||||
|
||||
# 新增字段
|
||||
template_id: Mapped[str | None] # 使用的评价模板 ID
|
||||
fund_type: Mapped[str | None] # 冗余存储,便于查询
|
||||
fund_lifecycle: Mapped[str | None]
|
||||
company_stage: Mapped[str | None]
|
||||
industry: Mapped[str | None]
|
||||
strategy: Mapped[str | None]
|
||||
custom_metrics_result: Mapped[dict | None] # 专属指标评分结果
|
||||
lp_focus_result: Mapped[dict | None] # LP 附加指标结果
|
||||
```
|
||||
|
||||
### 6.3 基金信息表(新增)
|
||||
|
||||
```python
|
||||
class Fund(Base):
|
||||
"""基金信息 — 管理基金类型和存续期。"""
|
||||
__tablename__ = "funds"
|
||||
|
||||
id: Mapped[str]
|
||||
tenant_id: Mapped[str]
|
||||
name: Mapped[str] # 基金名称
|
||||
fund_type: Mapped[str] # angel/early_vc/growth_vc/pe/cvc/fof/distress/esg
|
||||
strategy: Mapped[str] # growth/value/empowerment/turnaround
|
||||
|
||||
# 存续期信息
|
||||
established_date: Mapped[date] # 基金成立日
|
||||
total_lifespan_months: Mapped[int] # 总存续期(月)
|
||||
investment_period_months: Mapped[int] # 投资期(月)
|
||||
current_lifecycle: Mapped[str] # 当前阶段(自动计算)
|
||||
|
||||
# LP 构成
|
||||
lp_composition_json: Mapped[dict | None] # {government: 30%, market: 50%, corporate: 20%}
|
||||
|
||||
# 地域
|
||||
primary_market: Mapped[str | None] # china_mainland/us/sea/europe
|
||||
|
||||
# 状态
|
||||
is_active: Mapped[bool]
|
||||
```
|
||||
|
||||
### 6.4 企业-基金关联表(新增)
|
||||
|
||||
```python
|
||||
class CompanyFundLink(Base):
|
||||
"""企业-基金关联 — 一个企业可能被多支基金投资。"""
|
||||
__tablename__ = "company_fund_links"
|
||||
|
||||
id: Mapped[str]
|
||||
company_id: Mapped[str]
|
||||
fund_id: Mapped[str]
|
||||
investment_date: Mapped[date] # 投资日期
|
||||
investment_stage: Mapped[str] # 投资时企业阶段
|
||||
round: Mapped[str] # 轮次
|
||||
amount: Mapped[float] # 投资金额
|
||||
ownership_pct: Mapped[float] # 持股比例
|
||||
is_current: Mapped[bool] # 当前是否持有
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 预设配置
|
||||
|
||||
### 7.1 基金类型 × 存续期基础权重(32 个预设)
|
||||
|
||||
```python
|
||||
FUND_LIFECYCLE_WEIGHTS = {
|
||||
# (fund_type, lifecycle): {dimension: weight_multiplier}
|
||||
("angel", "investment"): {"financial": 0.5, "product_tech": 1.8, "org_talent": 1.5, "market": 1.2},
|
||||
("angel", "growth"): {"financial": 0.7, "product_tech": 1.5, "org_talent": 1.3, "market": 1.2},
|
||||
("angel", "exit_preparation"): {"financial": 1.0, "product_tech": 1.2, "org_talent": 1.0, "market": 1.0, "financing": 1.5},
|
||||
("angel", "liquidation"): {"financial": 1.5, "financing": 2.0, "product_tech": 0.8},
|
||||
|
||||
("early_vc", "investment"): {"financial": 0.7, "product_tech": 1.5, "market": 1.2, "customer_success": 0.8},
|
||||
("early_vc", "growth"): {"financial": 0.9, "product_tech": 1.3, "market": 1.2, "customer_success": 1.0},
|
||||
("early_vc", "exit_preparation"): {"financial": 1.3, "market": 1.0, "customer_success": 1.2, "financing": 1.5},
|
||||
("early_vc", "liquidation"): {"financial": 1.8, "financing": 2.0, "product_tech": 0.6},
|
||||
|
||||
("growth_vc", "investment"): {"financial": 1.0, "market": 1.3, "customer_success": 1.2},
|
||||
("growth_vc", "growth"): {"financial": 1.2, "market": 1.2, "customer_success": 1.3},
|
||||
("growth_vc", "exit_preparation"): {"financial": 1.5, "market": 1.0, "customer_success": 1.2, "financing": 1.5},
|
||||
("growth_vc", "liquidation"): {"financial": 2.0, "financing": 2.0, "market": 0.8},
|
||||
|
||||
("pe", "investment"): {"financial": 1.8, "governance": 1.5, "customer_success": 1.3, "product_tech": 0.5, "ai_commercial": 0.3},
|
||||
("pe", "growth"): {"financial": 2.0, "governance": 1.5, "customer_success": 1.3, "product_tech": 0.5},
|
||||
("pe", "exit_preparation"): {"financial": 2.5, "governance": 1.8, "financing": 1.5},
|
||||
("pe", "liquidation"): {"financial": 3.0, "financing": 2.0, "governance": 1.5},
|
||||
|
||||
("cvc", "investment"): {"synergy": 2.0, "market": 1.3, "product_tech": 1.2, "financial": 0.7},
|
||||
("cvc", "growth"): {"synergy": 1.8, "market": 1.2, "product_tech": 1.2, "financial": 0.8},
|
||||
("cvc", "exit_preparation"): {"synergy": 1.5, "market": 1.0, "financial": 1.2, "financing": 1.3},
|
||||
("cvc", "liquidation"): {"financial": 1.5, "financing": 1.5, "synergy": 1.0},
|
||||
|
||||
("distress", "investment"): {"financial": 2.5, "governance": 1.5, "product_tech": 0.5, "market": 0.5},
|
||||
("distress", "growth"): {"financial": 2.5, "governance": 1.5, "product_tech": 0.5},
|
||||
("distress", "exit_preparation"): {"financial": 3.0, "governance": 1.5, "financing": 1.5},
|
||||
("distress", "liquidation"): {"financial": 3.0, "financing": 2.0, "governance": 1.5},
|
||||
|
||||
("esg", "investment"): {"governance": 1.8, "data_compliance": 1.5, "product_tech": 1.2, "financial": 0.8},
|
||||
("esg", "growth"): {"governance": 1.5, "data_compliance": 1.3, "customer_success": 1.2},
|
||||
("esg", "exit_preparation"): {"governance": 1.8, "financial": 1.3, "financing": 1.3},
|
||||
("esg", "liquidation"): {"financial": 1.5, "governance": 1.5, "financing": 1.5},
|
||||
|
||||
# FOF 不直接评价单企业,使用默认权重
|
||||
("fof", "investment"): {},
|
||||
("fof", "growth"): {},
|
||||
("fof", "exit_preparation"): {},
|
||||
("fof", "liquidation"): {},
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 企业阶段系数(5 个预设)
|
||||
|
||||
```python
|
||||
STAGE_MULTIPLIERS = {
|
||||
"seed": {"financial": 0.6, "product_tech": 1.8, "org_talent": 1.5, "market": 0.5, "governance": 0.6, "customer_success": 0.5},
|
||||
"a": {"financial": 0.8, "product_tech": 1.5, "org_talent": 1.2, "market": 1.0, "governance": 0.7, "customer_success": 0.8},
|
||||
"b": {"financial": 1.2, "market": 1.3, "customer_success": 1.3, "product_tech": 1.0, "governance": 1.0},
|
||||
"c": {"financial": 1.5, "market": 1.3, "governance": 1.3, "customer_success": 1.3, "product_tech": 0.8},
|
||||
"pre_ipo": {"financial": 1.5, "governance": 1.8, "customer_success": 1.3, "market": 1.0, "product_tech": 0.8},
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 产业赛道配置(6 个预设)
|
||||
|
||||
```python
|
||||
INDUSTRY_CONFIGS = {
|
||||
"ai": {
|
||||
"enabled": ["financial_score", "operational_score", "ai_commercial_score", "ai_cost_score",
|
||||
"org_talent_score", "product_tech_score", "market_compete_score", "governance_score",
|
||||
"financing_score", "synergy_score", "ai_model_product_score",
|
||||
"data_compliance_score", "team_tech_score", "customer_success_score"],
|
||||
"disabled": [],
|
||||
"custom_metrics": ["model_accuracy", "inference_cost", "api_call_volume", "poc_conversion_rate"],
|
||||
},
|
||||
"saas": {
|
||||
"enabled": ["financial_score", "operational_score", "ai_commercial_score",
|
||||
"org_talent_score", "product_tech_score", "market_compete_score", "governance_score",
|
||||
"financing_score", "synergy_score", "data_compliance_score",
|
||||
"team_tech_score", "customer_success_score"],
|
||||
"disabled": ["ai_cost_score", "ai_model_product_score"],
|
||||
"custom_metrics": ["arr_growth", "net_revenue_retention", "cac_payback", "rule_of_40"],
|
||||
},
|
||||
"hardware": {
|
||||
"enabled": ["financial_score", "operational_score",
|
||||
"org_talent_score", "product_tech_score", "market_compete_score", "governance_score",
|
||||
"financing_score", "synergy_score", "data_compliance_score",
|
||||
"team_tech_score", "customer_success_score"],
|
||||
"disabled": ["ai_commercial_score", "ai_cost_score", "ai_model_product_score"],
|
||||
"custom_metrics": ["patent_count", "tape_out_progress", "yield_rate", "rd_investment_ratio"],
|
||||
},
|
||||
"biotech": {
|
||||
"enabled": ["financial_score", "operational_score",
|
||||
"org_talent_score", "product_tech_score", "governance_score",
|
||||
"financing_score", "synergy_score", "data_compliance_score",
|
||||
"team_tech_score"],
|
||||
"disabled": ["ai_commercial_score", "ai_cost_score", "ai_model_product_score", "market_compete_score", "customer_success_score"],
|
||||
"custom_metrics": ["clinical_stage", "pipeline_progress", "regulatory_milestone", "patent_landscape"],
|
||||
},
|
||||
"consumer": {
|
||||
"enabled": ["financial_score", "operational_score",
|
||||
"org_talent_score", "product_tech_score", "market_compete_score", "governance_score",
|
||||
"financing_score", "synergy_score", "team_tech_score", "customer_success_score"],
|
||||
"disabled": ["ai_commercial_score", "ai_cost_score", "ai_model_product_score", "data_compliance_score"],
|
||||
"custom_metrics": ["gmv", "repurchase_rate", "brand_index", "channel_coverage"],
|
||||
},
|
||||
"fintech": {
|
||||
"enabled": ["financial_score", "operational_score", "ai_commercial_score",
|
||||
"org_talent_score", "product_tech_score", "market_compete_score", "governance_score",
|
||||
"financing_score", "synergy_score", "ai_model_product_score",
|
||||
"data_compliance_score", "team_tech_score", "customer_success_score"],
|
||||
"disabled": ["ai_cost_score"],
|
||||
"custom_metrics": ["license_progress", "risk_control_score", "compliance_events", "npl_ratio"],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 投资策略微调(4 个预设)
|
||||
|
||||
```python
|
||||
STRATEGY_ADJUSTMENTS = {
|
||||
"growth": {"market": +5, "product_tech": +5, "customer_success": +5, "financial": -5, "governance": -5},
|
||||
"value": {"financial": +5, "governance": +5, "customer_success": +5, "market": -5, "product_tech": -5},
|
||||
"empowerment": {"synergy": +5, "org_talent": +5, "product_tech": +5, "financial": -5, "market": -5},
|
||||
"turnaround": {"financial": +5, "governance": +5, "market": -5, "product_tech": -5},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 模板组合数量
|
||||
|
||||
| 配置单元类型 | 预设数 | 说明 |
|
||||
|---|---|---|
|
||||
| 基金类型 × 存续期 | 32 | 8 × 4 基础权重模板 |
|
||||
| 企业阶段 | 5 | 阶段系数 |
|
||||
| 产业赛道 | 6 | 维度裁剪 + 专属指标 |
|
||||
| 投资策略 | 4 | ±5% 微调 |
|
||||
| 投资人类型 | 3 | 展示模板 |
|
||||
| **合计** | **50** | 运行时动态组合 |
|
||||
|
||||
无需 8×4×5×6×4×3 = 11,520 个模板。
|
||||
|
||||
---
|
||||
|
||||
## 9. API 设计
|
||||
|
||||
### 9.1 获取评价模板
|
||||
|
||||
```
|
||||
GET /api/v1/evaluation/templates?fund_type=early_vc&lifecycle=growth&stage=a&industry=ai&strategy=growth
|
||||
```
|
||||
|
||||
### 9.2 计算评分
|
||||
|
||||
```
|
||||
POST /api/v1/evaluation/score
|
||||
{
|
||||
"company_id": "xxx",
|
||||
"template_id": "yyy", // 可选,不传则自动匹配
|
||||
"structured_data": { ... } // 月报结构化数据
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3 查看评分历史
|
||||
|
||||
```
|
||||
GET /api/v1/evaluation/scores?company_id=xxx&template_id=yyy
|
||||
```
|
||||
|
||||
### 9.4 管理模板
|
||||
|
||||
```
|
||||
POST /api/v1/evaluation/templates # 创建模板
|
||||
PUT /api/v1/evaluation/templates/:id # 更新模板
|
||||
GET /api/v1/evaluation/templates # 列表
|
||||
DELETE /api/v1/evaluation/templates/:id # 删除模板
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 实现计划
|
||||
|
||||
| 阶段 | 任务 | 优先级 |
|
||||
|---|---|---|
|
||||
| Phase 1 | 数据模型:Fund / CompanyFundLink / EvaluationTemplate 表 | P0 |
|
||||
| Phase 2 | 权重计算引擎:6 轴动态权重合并 + 归一化 | P0 |
|
||||
| Phase 3 | 50 个预设配置写入数据库 | P0 |
|
||||
| Phase 4 | API:模板管理 + 评分计算 + 历史查询 | P1 |
|
||||
| Phase 5 | 前端:模板配置页面 + 评分对比视图 | P1 |
|
||||
| Phase 6 | 修饰因子:LP 附加指标 + 地域基准校准 | P2 |
|
||||
| Phase 7 | 专属指标采集:各赛道 3-5 个原子指标接入 | P2 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 与现有系统的兼容性
|
||||
|
||||
| 现有功能 | 兼容方案 |
|
||||
|---|---|
|
||||
| `health_calculator.py` 14 维度计算 | 保留,权重从固定改为动态读取模板 |
|
||||
| `HealthScore` 模型 | 新增 `template_id` 等字段,旧数据 `template_id = NULL` |
|
||||
| 前端 `HealthRadar` 雷达图 | 保留,`dimensions` 参数从模板动态传入 |
|
||||
| 月报 AI 解析流程 | 保留,解析后增加模板匹配 + 动态权重计算步骤 |
|
||||
| Dashboard 汇总 | 保留,跨企业汇总时按各自模板计算 |
|
||||
Reference in New Issue
Block a user