feat(backend): AI 服务层 — 千问流式 LLM + 月报解析 + 健康度计算 + 风险检测 + Copilot

- LLM 客户端:全部 SSE 流式输出,兼容 OpenAI 接口
- AI 月报解析:SSE 流式端点 POST /reports/{id}/parse
- 健康度计算引擎:四维评分(财务/经营/AI商业化/AI成本)
- 风险自动检测引擎:6 条规则自动检测指标越界
- AI Copilot:SSE 流式对话 POST /copilot/chat
- 权限中间件:角色级 + 字段级权限控制
- 测试:21 个新测试(健康度 8 + 风险检测 8 + 权限 5),总计 64 passed
This commit is contained in:
selfrelease
2026-07-18 22:16:40 +08:00
parent 94be6189e9
commit 7ec4fb0747
15 changed files with 1244 additions and 52 deletions
+5 -3
View File
@@ -26,9 +26,11 @@ class Settings(BaseSettings):
jwt_access_token_ttl_minutes: int = 120
jwt_refresh_token_ttl_days: int = 7
# AI / Ollama
ollama_base_url: str = "http://localhost:11434"
ollama_model: str = "qwen2.5:7b"
# AI / LLM(千问 DashScope OpenAI 兼容模式)
llm_api_key: str = ""
llm_base_url: str = "https://dashscope.aliyuncs.com/compatible-mode/v1"
llm_model: str = "qwen-plus"
llm_timeout_seconds: int = 60
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
+91
View File
@@ -0,0 +1,91 @@
"""角色级 + 字段级权限中间件。
基于用户角色控制 API 访问权限和数据可见性。
"""
from fastapi import Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.dependencies import get_current_user
from app.models.user import User
# 角色层级
ROLE_HIERARCHY = {
"admin": 100,
"investor": 50,
"founder": 20,
}
def require_role(*allowed_roles: str):
"""角色级权限依赖工厂。
用法:
@router.get("/admin-only", dependencies=[Depends(require_role("admin"))])
"""
async def _check(user: User = Depends(get_current_user)) -> User:
if user.role not in allowed_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"需要角色: {', '.join(allowed_roles)},当前角色: {user.role}",
)
return user
return _check
def require_min_role(min_role: str):
"""最低角色层级权限依赖工厂。
用法:
@router.get("/investor+", dependencies=[Depends(require_min_role("investor"))])
"""
min_level = ROLE_HIERARCHY.get(min_role, 0)
async def _check(user: User = Depends(get_current_user)) -> User:
user_level = ROLE_HIERARCHY.get(user.role, 0)
if user_level < min_level:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"需要最低角色: {min_role},当前角色: {user.role}",
)
return user
return _check
# 字段级权限:不同角色可见的字段
FIELD_VISIBILITY = {
"founder": {
"company": ["id", "name", "industry", "stage", "description", "website"],
"report": ["id", "company_id", "period_year", "period_month", "status", "raw_content"],
},
"investor": {
"company": ["*"], # 全部可见
"report": ["*"],
},
"admin": {
"company": ["*"],
"report": ["*"],
},
}
def filter_fields(
resource: str,
data: dict,
user: User,
) -> dict:
"""根据用户角色过滤返回字段。
Args:
resource: 资源名称(company / report 等)
data: 原始数据字典
user: 当前用户
Returns:
过滤后的数据字典
"""
allowed = FIELD_VISIBILITY.get(user.role, {}).get(resource, ["*"])
if "*" in allowed:
return data
return {k: v for k, v in data.items() if k in allowed}