feat(backend): 6-axis dynamic evaluation system with weight engine and template management

This commit is contained in:
selfrelease
2026-07-19 20:59:31 +08:00
parent f4ddcab2ca
commit de53a252e4
11 changed files with 2145 additions and 1 deletions
+10 -1
View File
@@ -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")
+5
View File
@@ -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",
+60
View File
@@ -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),
)
+95
View File
@@ -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)
)
+11
View File
@@ -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 附加指标结果")
+451
View File
@@ -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,
})
+313
View File
@@ -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
+154
View File
@@ -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 默认使用 growthinvestor_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))
+299
View File
@@ -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
+226
View File
@@ -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} 缺少映射"
+521
View File
@@ -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+ | 赛道赌注、团队潜力 | 产品↑↑、团队↑↑、市场天花板↑;财务容忍度极高 |
| 早期VCA轮) | 5-8年 | 5-10x | PMF 验证、增长引擎 | 产品↑、市场↑、客户成功↑ |
| 成长期VCB/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 汇总 | 保留,跨企业汇总时按各自模板计算 |