Compare commits
42 Commits
f09aa33589
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 50cc92c2d7 | |||
| 3bae5fbfc1 | |||
| af91d843d8 | |||
| 006fd7de7e | |||
| c3232f73c8 | |||
| 4265f4bc4c | |||
| e81aa6828e | |||
| 8b8926bde3 | |||
| 3d22bd8a3d | |||
| 0197f89018 | |||
| a24ccd01c7 | |||
| 7b0b538b37 | |||
| 8138fb5540 | |||
| 4980ebcd30 | |||
| 4851b7e427 | |||
| e3ec98f63a | |||
| de53a252e4 | |||
| f4ddcab2ca | |||
| 3a905da35b | |||
| 778c2e8c5c | |||
| 44cbdc9d62 | |||
| a4044baa31 | |||
| 129210405d | |||
| 956270d14d | |||
| 947642919b | |||
| fad458b2a7 | |||
| 734a16a7f3 | |||
| 2c429ca1a2 | |||
| 5b34651e41 | |||
| 374981609b | |||
| 63031dd183 | |||
| 53ae2059c5 | |||
| 7ec4fb0747 | |||
| 94be6189e9 | |||
| 4432d47ff9 | |||
| 353a52a401 | |||
| 263f474c90 | |||
| 0bcaafd0eb | |||
| 8fe8047429 | |||
| a95f63c24c | |||
| 2be0778ec7 | |||
| 51feae55ba |
@@ -0,0 +1,28 @@
|
||||
# 环境变量示例
|
||||
# 复制为 .env 并修改实际值
|
||||
|
||||
# ===== 数据库 =====
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/aiportpilot
|
||||
|
||||
# ===== Redis =====
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
|
||||
# ===== JWT =====
|
||||
JWT_SECRET_KEY=change-me-in-production
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_ACCESS_TOKEN_TTL_MINUTES=120
|
||||
JWT_REFRESH_TOKEN_TTL_DAYS=7
|
||||
|
||||
# ===== AI / LLM(千问 DashScope) =====
|
||||
LLM_API_KEY=your-dashscope-api-key
|
||||
LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
LLM_MODEL=qwen-plus
|
||||
LLM_TIMEOUT_SECONDS=60
|
||||
|
||||
# ===== 前端 =====
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
|
||||
|
||||
# ===== 应用 =====
|
||||
APP_ENV=development
|
||||
APP_DEBUG=true
|
||||
APP_LOG_LEVEL=info
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
/lib/
|
||||
/lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual Environment
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# UV
|
||||
uv.lock
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
.pnpm-store/
|
||||
|
||||
# Next.js
|
||||
.next/
|
||||
out/
|
||||
build/
|
||||
next-env.d.ts
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
|
||||
# Database
|
||||
*.sql.bak
|
||||
backups/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Test
|
||||
.coverage
|
||||
htmlcov/
|
||||
.pytest_cache/
|
||||
coverage/
|
||||
|
||||
# Misc
|
||||
*.tmp
|
||||
*.bak
|
||||
@@ -0,0 +1,48 @@
|
||||
# AGENTS.md — Agent 协作规则
|
||||
|
||||
## 核心原则
|
||||
|
||||
1. **先想再写**:任何代码变更前,确认当前处于方法论七步流程的哪一步
|
||||
2. **先测再码**:TDD 强制,RED-GREEN-REFACTOR
|
||||
3. **小步快跑**:任务粒度 2-5 分钟,每个 RED-GREEN 循环提交一次
|
||||
4. **不删旧代码**:修改时保留无关代码,用快照隔离历史
|
||||
5. **中文沟通**:所有对话、注释、文档使用中文
|
||||
|
||||
## 分工
|
||||
|
||||
| Agent | 职责 | 触发时机 |
|
||||
|---|---|---|
|
||||
| Brainstorm Agent | 需求澄清、方案探索 | 新功能提出时 |
|
||||
| Plan Agent | 任务拆解、文件路径规划 | 设计批准后 |
|
||||
| TDD Agent | 写测试 → 写实现 → 重构 | 任务执行时 |
|
||||
| Review Agent | 代码审查、规格符合度检查 | 每个任务完成后 |
|
||||
| Debug Agent | 根因分析、复现测试 | Bug 出现时 |
|
||||
|
||||
## 禁止事项
|
||||
|
||||
- 禁止跳过测试直接写代码
|
||||
- 禁止删除无关代码
|
||||
- 禁止 `alert()/confirm()`
|
||||
- 禁止硬编码 hex 色值
|
||||
- 禁止多 UI 库混用
|
||||
- 禁止 `@skip/it.only` 进 PR
|
||||
- 禁止 mock DB 跑集成测试
|
||||
|
||||
## 提交规范
|
||||
|
||||
```
|
||||
type(scope): description
|
||||
|
||||
type ∈ feat/fix/refactor/test/docs/chore
|
||||
scope ∈ frontend/backend/db/docs/infra
|
||||
```
|
||||
|
||||
## 文件路径约定
|
||||
|
||||
- 前端页面:`frontend/src/app/(role)/page.tsx`
|
||||
- 前端组件:`frontend/src/components/`
|
||||
- 后端路由:`backend/app/routers/`
|
||||
- 后端模型:`backend/app/models/`
|
||||
- 后端服务:`backend/app/services/`
|
||||
- 测试:`backend/tests/` / `frontend/__tests__/`
|
||||
- 文档:`docs/`
|
||||
@@ -0,0 +1,244 @@
|
||||
# 应用构建方法论 v3.0(融合 Superpowers)
|
||||
|
||||
> 基于 v2.5 + [obra/superpowers](https://github.com/obra/superpowers) 流程纪律,针对 AIPortPilot 项目优化。
|
||||
> 核心理念:**结构化 > 自由发挥 / 快照 > 覆盖 / 演进 > 重写 / 先想再写 / 先测再码**
|
||||
|
||||
---
|
||||
|
||||
## 一、七步开发流程(强制执行)
|
||||
|
||||
每一步都是**必须执行的工作流**,不是建议。Agent 在任何任务前先检查当前处于哪一步。
|
||||
|
||||
### Step 1:Brainstorming(头脑风暴)
|
||||
|
||||
**触发**:用户提出新功能/新模块需求时
|
||||
**动作**:
|
||||
- 不急着写代码,先苏格拉底式提问
|
||||
- 探索替代方案,权衡取舍
|
||||
- 分段展示设计,每段短到用户能读完就消化
|
||||
- 产出:`docs/1-prd.md`(产品设计文档)
|
||||
**完成标志**:用户对 PRD 签字确认
|
||||
|
||||
### Step 2:Design Document(设计文档)
|
||||
|
||||
**触发**:PRD 签字后
|
||||
**动作**:
|
||||
- 技术架构设计(技术栈、数据模型、API 设计、UI/UX 方案)
|
||||
- 产出:`docs/1-prd.md` 中的技术设计章节
|
||||
**完成标志**:用户对技术方案签字确认
|
||||
|
||||
### Step 3:Writing Plans(任务拆解)
|
||||
|
||||
**触发**:设计批准后
|
||||
**动作**:
|
||||
- 将工作拆成 **2-5 分钟** 的小任务
|
||||
- 每个任务有:精确文件路径、完整代码描述、验证步骤
|
||||
- 任务之间无循环依赖,可并行标注
|
||||
- 产出:`docs/2-task.md`(任务清单)
|
||||
**完成标志**:用户对任务清单签字确认
|
||||
|
||||
### Step 4:Feature Branch(特性分支)
|
||||
|
||||
**触发**:任务清单批准后
|
||||
**动作**:
|
||||
- `git checkout -b feature/{module-name}`
|
||||
- 确保干净测试基线
|
||||
**完成标志**:分支创建成功
|
||||
|
||||
### Step 5:TDD Implementation(测试驱动实现)
|
||||
|
||||
**触发**:分支创建后
|
||||
**动作**:
|
||||
- **RED**:先写失败测试,运行确认失败
|
||||
- **GREEN**:写最小代码让测试通过
|
||||
- **REFACTOR**:重构,保持测试绿色
|
||||
- **COMMIT**:每个 RED-GREEN 循环提交一次
|
||||
- **禁止**:先写代码后补测试
|
||||
**完成标志**:所有任务测试通过
|
||||
|
||||
### Step 6:Code Review(代码审查)
|
||||
|
||||
**触发**:每个任务完成后
|
||||
**动作**:
|
||||
- 对照计划检查规格符合度
|
||||
- 检查代码质量(命名、结构、安全、性能)
|
||||
- 严重问题阻断进度,必须修复后继续
|
||||
**完成标志**:审查通过
|
||||
|
||||
### Step 7:Finishing Branch(收尾合并)
|
||||
|
||||
**触发**:所有任务完成且审查通过
|
||||
**动作**:
|
||||
- 运行全量测试
|
||||
- 提供选项:合并到 master / 创建 PR / 保留分支 / 丢弃
|
||||
- 合并后清理分支
|
||||
**完成标志**:代码进入 master
|
||||
|
||||
---
|
||||
|
||||
## 二、三原则
|
||||
|
||||
1. **结构化 > 自由发挥**:先文档后代码,先设计后实现
|
||||
2. **快照 > 覆盖**:用 `*_snapshot` JSON 保存历史,不删旧数据
|
||||
3. **演进 > 重写**:版本号 + `is_current` 指针,增量演进
|
||||
|
||||
---
|
||||
|
||||
## 三、文档骨架(必有)
|
||||
|
||||
| 文件 | 内容 | 何时写 |
|
||||
|---|---|---|
|
||||
| `README.md` | 项目介绍、技术栈、快速启动 | Step 2 |
|
||||
| `run.md` | 10 板块运维手册 | Step 2 |
|
||||
| `AGENTS.md` | Agent 协作规则 | Step 2 |
|
||||
| `docs/0-req.md` | 需求文档(从方案文档提炼) | Step 1 |
|
||||
| `docs/1-prd.md` | PRD + 技术设计 | Step 1-2 |
|
||||
| `docs/2-task.md` | 任务清单(2-5 min 粒度) | Step 3 |
|
||||
| `docs/daily/` | 日报 | 每日 |
|
||||
|
||||
### run.md 十板块
|
||||
|
||||
技术栈 / 首次准备 / 基础设施启停 / 应用启停 / DB 命令 / 排错 / 端口表 / env / 部署备份 / FAQ
|
||||
|
||||
### 铁律
|
||||
|
||||
- 命令可复制粘贴
|
||||
- 标注 `[Docker]` / `[Native]`
|
||||
- 危险操作标红
|
||||
- 版本号写死
|
||||
- 过时即同步
|
||||
|
||||
---
|
||||
|
||||
## 四、数据 8 铁律
|
||||
|
||||
1. 快照隔离历史(`*_snapshot` JSON)
|
||||
2. 版本号 + `is_current` 唯一指针,不删旧
|
||||
3. 内部状态机与用户可见状态分离
|
||||
4. 租户隔离走 `session.tenant_id`,不信任请求体
|
||||
5. 配置粒度对齐"谁应该决定"(全局/租户/用户)
|
||||
6. 字段演进:nullable + 默认值;枚举用字符串;时间戳 `*_at`
|
||||
7. 审计字段:`created_by/updated_by/deleted_by + *_at`;关键操作落 `audit_logs` ≥ 6 月
|
||||
8. UTC 存储;金额用 decimal/整数;禁止 float
|
||||
|
||||
---
|
||||
|
||||
## 五、安全与权限
|
||||
|
||||
- 密钥进 KMS/Vault,**绝不进 Git**,仅 commit `.env.example`
|
||||
- 后端必须独立校验权限,前端隐藏 ≠ 后端放权
|
||||
- PII 全链路脱敏(手机/身份证/邮箱),日志中用 `138****1234`
|
||||
- 注入防御:SQL 参数化 / 命令 `shell=False` / XSS 自动转义 + CSP / CSRF 走 SameSite
|
||||
- 限流熔断:登录 5次/min/IP,写接口按用户限流,429 带 `Retry-After`
|
||||
- JWT 短 TTL(≤2h)+ refresh;Cookie 必 HttpOnly+Secure+SameSite
|
||||
- **本项目特殊**:投后数据高度敏感,优先私有化部署,数据不出域
|
||||
|
||||
---
|
||||
|
||||
## 六、API 设计
|
||||
|
||||
- REST 资源命名(名词复数 + 层级),版本进 URL 不进 query
|
||||
- HTTP 状态码语义化,禁全 200 塞 error
|
||||
- 统一响应壳:`{code, message, data, trace_id, timestamp}`
|
||||
- 分页 `?page&page_size&sort&filter[k]=v`,大数据集用 keyset 游标
|
||||
- 写接口接受 `Idempotency-Key`,订单/支付**必须**
|
||||
- `trace_id` 全链路(网关→后端→DB→前端 `X-Trace-Id`)
|
||||
- OpenAPI 自动生成进 Git,废弃接口 `Deprecation` + `Sunset`
|
||||
|
||||
---
|
||||
|
||||
## 七、AI / 智能集成
|
||||
|
||||
- 接口抽象,业务面对自家"智能服务接口"
|
||||
- 多源容灾 + 兜底降级(规则/缓存/默认),不拖垮主流程
|
||||
- 强制 Structured Output / JSON Schema,禁编造
|
||||
- 决策证据化:输出 `score / confidence / evidence / concerns / fallback_used`
|
||||
- Prompt 进 Git,不只在 DB;可 diff、可回滚
|
||||
- 单次/用户/租户分级成本预算;缓存优先;慢路径异步化
|
||||
- 用户输入与系统 prompt **分离 role**,加分隔符 `<<<USER_INPUT>>>`,防注入
|
||||
- LLM 输出代码绝不直接 exec/eval,必经语法检查 + 沙箱
|
||||
- PII 进 LLM 前脱敏,响应再回填
|
||||
- `confidence < 0.6` 显式提示人工核对
|
||||
|
||||
---
|
||||
|
||||
## 八、客户端 + 多端 UIUX
|
||||
|
||||
### 三端角色化
|
||||
|
||||
- **投资人端(B 端专业)**:主色 `gray-900`,左 Sidebar(`w-52 sticky`)+ 内容 `bg-[#f8f9fb]`,信息密集
|
||||
- **创始人端(C 端温暖)**:主色 `indigo-600`,顶部 sticky Header(`h-14 bg-white/95 backdrop-blur`),渐变背景
|
||||
- **Admin 端(警示)**:Header `bg-slate-950` + 主色 `amber-400`,内容 `bg-slate-100`,必带 ADMIN 徽章
|
||||
|
||||
### 共享 Token
|
||||
|
||||
- Geist 字体 / oklch 色彩 / `--radius: 0.625rem`,**禁硬编码 hex**
|
||||
- shadcn + @base-ui + lucide + sonner + recharts,**禁多 UI 库混用**
|
||||
|
||||
### Layout 统一
|
||||
|
||||
- 容器 `container mx-auto max-w-7xl px-4`,Header 全应用 `h-14`
|
||||
|
||||
### 交互统一
|
||||
|
||||
- `<LoadingSpinner>` / `<EmptyState>` / sonner toast / `<Dialog>` 二次确认
|
||||
- 状态色三件套:`emerald(success) / amber(warning) / rose(destructive)`
|
||||
|
||||
### 响应式
|
||||
|
||||
- sm/md/lg/xl/2xl 五断点
|
||||
- Sidebar < md 折叠为抽屉/下拉,不丢功能入口
|
||||
|
||||
### a11y
|
||||
|
||||
- 键盘可达 / 对比度 ≥ 4.5:1 / 语义化标签 / aria-label,**禁 `<div onClick>`**
|
||||
|
||||
### 其他
|
||||
|
||||
- i18n:文案进 JSON,用 `Intl.*` 格式化
|
||||
- 报告页强制 `@media print`
|
||||
- 流式输出 RAF 批量刷新,禁每 token 触发 React 重渲染
|
||||
- **禁 `alert()/confirm()`、禁多 UI 库、禁硬编码主色 hex、禁三端共用 Header**
|
||||
|
||||
---
|
||||
|
||||
## 九、五防一兜底
|
||||
|
||||
1. 防异步穿越(AbortController / 引用保存)
|
||||
2. 防外部单点(超时+重试+熔断+多源)
|
||||
3. 防资源缺失(启动校验字体/翻译/配置)
|
||||
4. 防 API 弃用(季度内替换 + 灰度升级)
|
||||
5. 防数据丢失(事务 / 幂等键 / 持久化队列 / beforeunload)
|
||||
6. 一兜底:最差体验是"功能受限可用",不是白屏
|
||||
|
||||
---
|
||||
|
||||
## 十、部署运维
|
||||
|
||||
- 部署脚本"只清自己",禁 `pm2 delete all`、禁 `redis-cli FLUSHALL`、禁 `rm -rf /`
|
||||
- 跨子域 cookie 名独立 + `domain=.example.com`
|
||||
- DB migrate 走 CI/CD 自动应用
|
||||
- 备份:每天 2 次 + 滚动 7 天 + 双副本(本机 + 异地)+ 每月恢复演练
|
||||
- 监控 4 金指标:Latency / Traffic / Errors / Saturation
|
||||
- 灰度 1% → 10% → 50% → 100%,每档观察 ≥ 30 分钟
|
||||
- 回滚 SOP 必备,回滚比修复快
|
||||
|
||||
---
|
||||
|
||||
## 十一、测试纪律
|
||||
|
||||
- **70/20/10**:单元/集成/E2E
|
||||
- **TDD 强制**:RED-GREEN-REFACTOR,先写失败测试再写代码
|
||||
- 关键路径必覆盖(登录/支付/导出/权限/AI 主流程)
|
||||
- 集成测试用 testcontainers 跑真实 DB,禁 mock DB
|
||||
- 修 bug 必先写复现测试,禁删测试、禁 `@skip/it.only` 进 PR
|
||||
|
||||
---
|
||||
|
||||
## 十二、协作纪律
|
||||
|
||||
- 每步完成后更新 `docs/2-task.md` 状态
|
||||
- 日报写 `docs/daily/YYYY-MM-DD.md`
|
||||
- 重大决策记 `docs/decisions/`(ADR 格式)
|
||||
- 代码审查问题按严重度分级:Critical(阻断)/ Major(必须修)/ Minor(建议)
|
||||
- 提交信息格式:`type(scope): description`,type ∈ feat/fix/refactor/test/docs/chore
|
||||
@@ -0,0 +1,63 @@
|
||||
# AIPortPilot — AI+ Portfolio Operating System
|
||||
|
||||
> AI 投后管理与组合协同平台:投资人和创始人的共同操作系统。
|
||||
> 对投资收益负责,主动创造增长,持续进化管理知识。
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层 | 技术 | 版本 |
|
||||
|---|---|---|
|
||||
| 前端 | Next.js + React + TypeScript | 15.x / 19.x / 5.x |
|
||||
| UI | TailwindCSS + shadcn/ui + lucide + recharts | 4.x |
|
||||
| 后端 | Python + FastAPI | 3.12 / 0.115.x |
|
||||
| 数据库 | PostgreSQL + Redis | 16 / 7.x |
|
||||
| 向量库 | PgVector (PostgreSQL 扩展) | — |
|
||||
| AI | Ollama (本地推理) + LangChain | — |
|
||||
| 部署 | Docker Compose → K8s | — |
|
||||
|
||||
## 快速启动
|
||||
|
||||
```bash
|
||||
# [Docker] 一键启动全部基础设施
|
||||
docker compose up -d
|
||||
|
||||
# [Native] 后端
|
||||
cd backend && uv sync && uv run uvicorn app.main:app --reload --port 8000
|
||||
|
||||
# [Native] 前端
|
||||
cd frontend && pnpm install && pnpm dev
|
||||
```
|
||||
|
||||
详见 [run.md](./run.md)。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
AIPortPilot/
|
||||
├── frontend/ # Next.js 前端(投资人端 + 创始人端 + Admin)
|
||||
├── backend/ # FastAPI 后端
|
||||
├── docs/ # 文档(需求 / PRD / 任务 / 日报)
|
||||
├── docker-compose.yml # 开发环境编排
|
||||
├── BUILD-METHODOLOGY.md # 构建方法论 v3.0
|
||||
└── AI+投后管理方案v2.0.md # 原始方案文档
|
||||
```
|
||||
|
||||
## MVP 范围(Phase 1)
|
||||
|
||||
- 企业档案管理
|
||||
- 月报在线提交 + AI 解析
|
||||
- 通用健康度评分(财务 + 经营)
|
||||
- AI+ 专项健康度(商业化 + 成本)
|
||||
- 投资机构驾驶舱
|
||||
- 风险预警(指标越界)
|
||||
- 投后报告自动生成
|
||||
- 移动端:风险预警推送 + AI Copilot 基础问答
|
||||
|
||||
## 文档
|
||||
|
||||
- [构建方法论](./BUILD-METHODOLOGY.md)
|
||||
- [运维手册](./run.md)
|
||||
- [Agent 协作规则](./AGENTS.md)
|
||||
- [需求文档](./docs/0-req.md)
|
||||
- [PRD + 技术设计](./docs/1-prd.md)
|
||||
- [任务清单](./docs/2-task.md)
|
||||
@@ -0,0 +1,15 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pyproject.toml ./
|
||||
RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple \
|
||||
fastapi uvicorn[standard] sqlalchemy[asyncio] asyncpg alembic \
|
||||
pydantic pydantic-settings python-jose[cryptography] passlib[bcrypt] \
|
||||
python-multipart redis httpx structlog
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -0,0 +1,149 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts.
|
||||
# this is typically a path given in POSIX (e.g. forward slashes)
|
||||
# format, relative to the token %(here)s which refers to the location of this
|
||||
# ini file
|
||||
script_location = %(here)s/alembic
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||
# for all available tokens
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
|
||||
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory. for multiple paths, the path separator
|
||||
# is defined by "path_separator" below.
|
||||
prepend_sys_path = .
|
||||
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the tzdata library which can be installed by adding
|
||||
# `alembic[tz]` to the pip requirements.
|
||||
# string value is passed to ZoneInfo()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to <script_location>/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "path_separator"
|
||||
# below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
|
||||
|
||||
# path_separator; This indicates what character is used to split lists of file
|
||||
# paths, including version_locations and prepend_sys_path within configparser
|
||||
# files such as alembic.ini.
|
||||
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
|
||||
# to provide os-dependent path splitting.
|
||||
#
|
||||
# Note that in order to support legacy alembic.ini files, this default does NOT
|
||||
# take place if path_separator is not present in alembic.ini. If this
|
||||
# option is omitted entirely, fallback logic is as follows:
|
||||
#
|
||||
# 1. Parsing of the version_locations option falls back to using the legacy
|
||||
# "version_path_separator" key, which if absent then falls back to the legacy
|
||||
# behavior of splitting on spaces and/or commas.
|
||||
# 2. Parsing of the prepend_sys_path option falls back to the legacy
|
||||
# behavior of splitting on spaces, commas, or colons.
|
||||
#
|
||||
# Valid values for path_separator are:
|
||||
#
|
||||
# path_separator = :
|
||||
# path_separator = ;
|
||||
# path_separator = space
|
||||
# path_separator = newline
|
||||
#
|
||||
# Use os.pathsep. Default configuration used for new projects.
|
||||
path_separator = os
|
||||
|
||||
# set to 'true' to search source files recursively
|
||||
# in each "version_locations" directory
|
||||
# new in Alembic version 1.10
|
||||
# recursive_version_locations = false
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
# database URL. This is consumed by the user-maintained env.py script only.
|
||||
# other means of configuring database URLs may be customized within the env.py
|
||||
# file.
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
|
||||
# hooks = ruff
|
||||
# ruff.type = module
|
||||
# ruff.module = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Alternatively, use the exec runner to execute a binary found on your PATH
|
||||
# hooks = ruff
|
||||
# ruff.type = exec
|
||||
# ruff.executable = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration. This is also consumed by the user-maintained
|
||||
# env.py script only.
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARNING
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARNING
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
@@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
||||
@@ -0,0 +1,77 @@
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import Base
|
||||
import app.models # noqa: F401 — 导入所有模型以便 Alembic 发现
|
||||
|
||||
config = context.config
|
||||
|
||||
# 使用项目配置的数据库 URL(同步驱动)
|
||||
config.set_main_option("sqlalchemy.url", settings.database_url.replace("+asyncpg", "+psycopg2"))
|
||||
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -0,0 +1,28 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,161 @@
|
||||
"""create core tables: tenants users companies reports health_scores risks audit_logs
|
||||
|
||||
Revision ID: 278c8cfa6042
|
||||
Revises:
|
||||
Create Date: 2026-07-18 21:49:38.409216
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '278c8cfa6042'
|
||||
down_revision: Union[str, Sequence[str], None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('tenants',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('name', sa.String(length=200), nullable=False, comment='租户名称'),
|
||||
sa.Column('type', sa.String(length=50), nullable=False, comment='租户类型:vc/cvc/gov/holdings'),
|
||||
sa.Column('config_json', postgresql.JSONB(astext_type=sa.Text()), nullable=True, comment='租户配置'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('companies',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('tenant_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('name', sa.String(length=200), nullable=False, comment='企业名称'),
|
||||
sa.Column('industry', sa.String(length=100), nullable=True, comment='行业'),
|
||||
sa.Column('stage', sa.String(length=50), nullable=True, comment='融资阶段:seed/a/b/c/ipo'),
|
||||
sa.Column('logo_url', sa.String(length=500), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True, comment='业务描述'),
|
||||
sa.Column('founded_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('total_funding', sa.String(length=50), nullable=True, comment='累计融资额'),
|
||||
sa.Column('website', sa.String(length=500), nullable=True),
|
||||
sa.Column('extra_json', postgresql.JSONB(astext_type=sa.Text()), nullable=True, comment='扩展字段'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['tenants.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_companies_tenant_id'), 'companies', ['tenant_id'], unique=False)
|
||||
op.create_table('users',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('tenant_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('email', sa.String(length=255), nullable=False),
|
||||
sa.Column('password_hash', sa.String(length=255), nullable=False),
|
||||
sa.Column('name', sa.String(length=100), nullable=False),
|
||||
sa.Column('role', sa.String(length=50), nullable=False, comment='角色:gp/partner/post_invest_lead/investor/founder/admin'),
|
||||
sa.Column('phone', sa.String(length=20), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['tenants.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
|
||||
op.create_index(op.f('ix_users_tenant_id'), 'users', ['tenant_id'], unique=False)
|
||||
op.create_table('audit_logs',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('tenant_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('user_id', sa.String(length=36), nullable=True),
|
||||
sa.Column('action', sa.String(length=100), nullable=False, comment='操作类型:login/view/create/update/delete/export/ai_call'),
|
||||
sa.Column('resource_type', sa.String(length=50), nullable=True, comment='资源类型'),
|
||||
sa.Column('resource_id', sa.String(length=36), nullable=True, comment='资源 ID'),
|
||||
sa.Column('detail_json', postgresql.JSONB(astext_type=sa.Text()), nullable=True, comment='操作详情'),
|
||||
sa.Column('ip', postgresql.INET(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['tenants.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_audit_logs_tenant_id'), 'audit_logs', ['tenant_id'], unique=False)
|
||||
op.create_table('health_scores',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('company_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('total_score', sa.Float(), nullable=False, comment='总分(0-100)'),
|
||||
sa.Column('financial_score', sa.Float(), nullable=True, comment='财务健康度'),
|
||||
sa.Column('operational_score', sa.Float(), nullable=True, comment='经营健康度'),
|
||||
sa.Column('ai_commercial_score', sa.Float(), nullable=True, comment='AI+ 商业化健康度'),
|
||||
sa.Column('ai_cost_score', sa.Float(), nullable=True, comment='AI+ 成本健康度'),
|
||||
sa.Column('trend', sa.String(length=20), nullable=True, comment='趋势:up/stable/down'),
|
||||
sa.Column('evidence_json', postgresql.JSONB(astext_type=sa.Text()), nullable=True, comment='评分依据'),
|
||||
sa.Column('recommendations_json', postgresql.JSONB(astext_type=sa.Text()), nullable=True, comment='建议动作'),
|
||||
sa.Column('calculated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_health_scores_company_id'), 'health_scores', ['company_id'], unique=False)
|
||||
op.create_table('monthly_reports',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('company_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('period_year', sa.Integer(), nullable=False, comment='报告年份'),
|
||||
sa.Column('period_month', sa.Integer(), nullable=False, comment='报告月份(1-12)'),
|
||||
sa.Column('status', sa.String(length=50), nullable=False, comment='状态:draft/submitted/ai_parsed/reviewed'),
|
||||
sa.Column('raw_content', sa.Text(), nullable=True, comment='原始内容'),
|
||||
sa.Column('structured_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True, comment='结构化指标数据'),
|
||||
sa.Column('ai_summary', sa.Text(), nullable=True, comment='AI 生成的摘要'),
|
||||
sa.Column('ai_concerns', postgresql.JSONB(astext_type=sa.Text()), nullable=True, comment='AI 关注点列表'),
|
||||
sa.Column('submitted_by', sa.String(length=36), nullable=True),
|
||||
sa.Column('submitted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('reviewed_by', sa.String(length=36), nullable=True),
|
||||
sa.Column('reviewed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ),
|
||||
sa.ForeignKeyConstraint(['reviewed_by'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['submitted_by'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_monthly_reports_company_id'), 'monthly_reports', ['company_id'], unique=False)
|
||||
op.create_table('risk_events',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('company_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('type', sa.String(length=50), nullable=False, comment='风险类型:financial/operational/org/ai_specific'),
|
||||
sa.Column('severity', sa.String(length=20), nullable=False, comment='严重程度:low/medium/high/critical'),
|
||||
sa.Column('status', sa.String(length=20), nullable=False, comment='状态:open/assigned/in_progress/resolved/closed'),
|
||||
sa.Column('title', sa.String(length=200), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('evidence_json', postgresql.JSONB(astext_type=sa.Text()), nullable=True, comment='证据链'),
|
||||
sa.Column('suggested_action', sa.Text(), nullable=True, comment='建议动作'),
|
||||
sa.Column('assigned_to', sa.String(length=36), nullable=True),
|
||||
sa.Column('due_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('identified_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('closed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['assigned_to'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['company_id'], ['companies.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_risk_events_company_id'), 'risk_events', ['company_id'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_risk_events_company_id'), table_name='risk_events')
|
||||
op.drop_table('risk_events')
|
||||
op.drop_index(op.f('ix_monthly_reports_company_id'), table_name='monthly_reports')
|
||||
op.drop_table('monthly_reports')
|
||||
op.drop_index(op.f('ix_health_scores_company_id'), table_name='health_scores')
|
||||
op.drop_table('health_scores')
|
||||
op.drop_index(op.f('ix_audit_logs_tenant_id'), table_name='audit_logs')
|
||||
op.drop_table('audit_logs')
|
||||
op.drop_index(op.f('ix_users_tenant_id'), table_name='users')
|
||||
op.drop_index(op.f('ix_users_email'), table_name='users')
|
||||
op.drop_table('users')
|
||||
op.drop_index(op.f('ix_companies_tenant_id'), table_name='companies')
|
||||
op.drop_table('companies')
|
||||
op.drop_table('tenants')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1 @@
|
||||
"""AIPortPilot 后端应用包。"""
|
||||
@@ -0,0 +1 @@
|
||||
"""应用配置模块。"""
|
||||
@@ -0,0 +1,47 @@
|
||||
"""审计日志装饰器。
|
||||
|
||||
自动记录 CREATE/UPDATE/DELETE 操作。
|
||||
"""
|
||||
|
||||
import functools
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.audit import AuditLog
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def log_audit(
|
||||
db: AsyncSession,
|
||||
user_id: str,
|
||||
action: str,
|
||||
target_type: str,
|
||||
target_id: str,
|
||||
detail: dict | None = None,
|
||||
tenant_id: str | None = None,
|
||||
) -> None:
|
||||
"""记录审计日志。
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
user_id: 操作用户 ID
|
||||
action: 操作类型(login/view/create/update/delete/export/ai_call)
|
||||
target_type: 资源类型(映射到 resource_type 字段)
|
||||
target_id: 资源 ID(映射到 resource_id 字段)
|
||||
detail: 操作详情字典
|
||||
tenant_id: 租户 ID
|
||||
"""
|
||||
audit = AuditLog(
|
||||
tenant_id=tenant_id or "",
|
||||
user_id=user_id,
|
||||
action=action,
|
||||
resource_type=target_type,
|
||||
resource_id=target_id,
|
||||
detail_json=detail,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(audit)
|
||||
await db.flush()
|
||||
@@ -0,0 +1,49 @@
|
||||
"""缓存装饰器。
|
||||
|
||||
为热点接口自动添加 Redis 缓存。
|
||||
"""
|
||||
|
||||
import functools
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import Any, Callable
|
||||
|
||||
from app.core.redis import cache_get, cache_set
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def cached(prefix: str, ttl: int = 300):
|
||||
"""缓存装饰器 — 自动缓存函数返回值。
|
||||
|
||||
Args:
|
||||
prefix: 缓存键前缀
|
||||
ttl: 缓存过期时间(秒)
|
||||
"""
|
||||
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@functools.wraps(func)
|
||||
async def wrapper(*args, **kwargs) -> Any:
|
||||
# 生成缓存键
|
||||
key_parts = [prefix]
|
||||
for arg in args[1:]: # 跳过 self/db
|
||||
key_parts.append(str(arg))
|
||||
for k, v in sorted(kwargs.items()):
|
||||
key_parts.append(f"{k}={v}")
|
||||
cache_key = hashlib.md5(":".join(key_parts).encode()).hexdigest()
|
||||
|
||||
# 尝试获取缓存
|
||||
cached = await cache_get(f"{prefix}:{cache_key}")
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
# 执行函数
|
||||
result = await func(*args, **kwargs)
|
||||
|
||||
# 写入缓存
|
||||
await cache_set(f"{prefix}:{cache_key}", result, ttl)
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
@@ -0,0 +1,38 @@
|
||||
"""应用配置。
|
||||
|
||||
从环境变量读取配置,支持 .env 文件。
|
||||
"""
|
||||
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""应用配置,从环境变量读取。"""
|
||||
|
||||
# 应用
|
||||
app_env: str = "development"
|
||||
app_debug: bool = True
|
||||
app_log_level: str = "info"
|
||||
|
||||
# 数据库
|
||||
database_url: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/aiportpilot"
|
||||
|
||||
# Redis
|
||||
redis_url: str = "redis://localhost:6379/0"
|
||||
|
||||
# JWT
|
||||
jwt_secret_key: str = "change-me-in-production"
|
||||
jwt_algorithm: str = "HS256"
|
||||
jwt_access_token_ttl_minutes: int = 120
|
||||
jwt_refresh_token_ttl_days: int = 7
|
||||
|
||||
# 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"}
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,37 @@
|
||||
"""PII 脱敏服务 — 手机/邮箱/身份证 日志脱敏。"""
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def mask_phone(phone: str) -> str:
|
||||
"""手机号脱敏:138****1234"""
|
||||
if len(phone) >= 11:
|
||||
return phone[:3] + "****" + phone[-4:]
|
||||
return phone
|
||||
|
||||
|
||||
def mask_email(email: str) -> str:
|
||||
"""邮箱脱敏:z***@example.com"""
|
||||
if "@" in email:
|
||||
name, domain = email.split("@", 1)
|
||||
if len(name) > 1:
|
||||
return name[0] + "***@" + domain
|
||||
return email
|
||||
|
||||
|
||||
def mask_id_card(id_card: str) -> str:
|
||||
"""身份证脱敏:110***********1234"""
|
||||
if len(id_card) >= 18:
|
||||
return id_card[:3] + "*" * 11 + id_card[-4:]
|
||||
return id_card
|
||||
|
||||
|
||||
def mask_pii(text: str) -> str:
|
||||
"""自动识别并脱敏文本中的 PII 信息。"""
|
||||
# 手机号
|
||||
text = re.sub(r"1[3-9]\d{9}", lambda m: mask_phone(m.group()), text)
|
||||
# 邮箱
|
||||
text = re.sub(r"[\w.+-]+@[\w-]+\.[\w.-]+", lambda m: mask_email(m.group()), text)
|
||||
# 身份证(18位)
|
||||
text = re.sub(r"\d{17}[\dXx]", lambda m: mask_id_card(m.group()), text)
|
||||
return text
|
||||
@@ -0,0 +1,43 @@
|
||||
"""数据库连接管理。
|
||||
|
||||
提供 SQLAlchemy 异步 engine 和 session 工厂。
|
||||
"""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
engine = create_async_engine(
|
||||
settings.database_url,
|
||||
echo=settings.app_debug,
|
||||
pool_size=10,
|
||||
max_overflow=20,
|
||||
)
|
||||
|
||||
async_session_factory = async_sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
"""SQLAlchemy ORM 基类。"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
"""获取数据库 session 的依赖注入函数。"""
|
||||
async with async_session_factory() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await session.close()
|
||||
@@ -0,0 +1,66 @@
|
||||
"""认证依赖注入。
|
||||
|
||||
提供当前用户依赖、权限校验。
|
||||
"""
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import decode_token
|
||||
from app.models.user import User
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> User:
|
||||
"""从 JWT token 中解析当前用户。"""
|
||||
token = credentials.credentials
|
||||
try:
|
||||
payload = decode_token(token)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的认证凭证",
|
||||
)
|
||||
|
||||
if payload.get("type") != "access":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的 token 类型",
|
||||
)
|
||||
|
||||
user_id = payload.get("sub")
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的 token 内容",
|
||||
)
|
||||
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户不存在或已禁用",
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def require_role(*roles: str):
|
||||
"""角色权限校验依赖工厂。"""
|
||||
async def check_role(user: User = Depends(get_current_user)) -> User:
|
||||
if user.role not in roles:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"需要角色:{', '.join(roles)}",
|
||||
)
|
||||
return user
|
||||
return check_role
|
||||
@@ -0,0 +1,23 @@
|
||||
"""敏感字段加密存储。"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
def encrypt_field(value: str) -> str:
|
||||
"""加密敏感字段(简化版 — 实际应使用 KMS/Vault)。"""
|
||||
key = settings.jwt_secret_key.encode()
|
||||
data = value.encode()
|
||||
# XOR 加密(简化版,生产环境应使用 AES)
|
||||
encrypted = bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
|
||||
return base64.b64encode(encrypted).decode()
|
||||
|
||||
|
||||
def decrypt_field(encrypted: str) -> str:
|
||||
"""解密敏感字段。"""
|
||||
key = settings.jwt_secret_key.encode()
|
||||
data = base64.b64decode(encrypted)
|
||||
decrypted = bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
|
||||
return decrypted.decode()
|
||||
@@ -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}
|
||||
@@ -0,0 +1,41 @@
|
||||
"""限流中间件 — 登录 5 次/min + 写接口按用户限流。"""
|
||||
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
from fastapi import HTTPException, Request, status
|
||||
|
||||
|
||||
class RateLimiter:
|
||||
"""简单的内存限流器。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._requests: dict[str, list[float]] = defaultdict(list)
|
||||
|
||||
def check(self, key: str, max_requests: int, window_seconds: int) -> bool:
|
||||
"""检查是否超过限流阈值。"""
|
||||
now = time.time()
|
||||
window_start = now - window_seconds
|
||||
|
||||
# 清理过期记录
|
||||
self._requests[key] = [t for t in self._requests[key] if t > window_start]
|
||||
|
||||
if len(self._requests[key]) >= max_requests:
|
||||
return False
|
||||
|
||||
self._requests[key].append(now)
|
||||
return True
|
||||
|
||||
|
||||
rate_limiter = RateLimiter()
|
||||
|
||||
|
||||
async def login_rate_limit(request: Request) -> None:
|
||||
"""登录接口限流 — 5 次/min/IP。"""
|
||||
client_ip = request.client.host if request.client else "unknown"
|
||||
if not rate_limiter.check(f"login:{client_ip}", max_requests=5, window_seconds=60):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
||||
detail="登录尝试过于频繁,请稍后再试",
|
||||
headers={"Retry-After": "60"},
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Redis 连接与缓存工具。"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import redis.asyncio as redis
|
||||
_redis_client = redis.from_url(settings.redis_url, decode_responses=True)
|
||||
except ImportError:
|
||||
_redis_client = None
|
||||
logger.warning("redis 未安装,缓存功能不可用")
|
||||
|
||||
|
||||
async def cache_get(key: str) -> Any | None:
|
||||
"""从 Redis 获取缓存。"""
|
||||
if not _redis_client:
|
||||
return None
|
||||
try:
|
||||
data = await _redis_client.get(key)
|
||||
return json.loads(data) if data else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def cache_set(key: str, value: Any, ttl: int = 300) -> None:
|
||||
"""设置 Redis 缓存。"""
|
||||
if not _redis_client:
|
||||
return
|
||||
try:
|
||||
await _redis_client.setex(key, ttl, json.dumps(value, default=str))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def cache_delete(key: str) -> None:
|
||||
"""删除 Redis 缓存。"""
|
||||
if not _redis_client:
|
||||
return
|
||||
try:
|
||||
await _redis_client.delete(key)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,46 @@
|
||||
"""安全模块:JWT 生成与验证、密码哈希。"""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
from jose import jwt
|
||||
from passlib.context import CryptContext
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
"""密码哈希。"""
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""验证密码。"""
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def create_access_token(subject: str, extra_claims: dict[str, Any] | None = None) -> str:
|
||||
"""生成 JWT access token。"""
|
||||
expire = datetime.now(timezone.utc) + timedelta(
|
||||
minutes=settings.jwt_access_token_ttl_minutes
|
||||
)
|
||||
payload: dict[str, Any] = {"sub": subject, "exp": expire, "type": "access"}
|
||||
if extra_claims:
|
||||
payload.update(extra_claims)
|
||||
return jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)
|
||||
|
||||
|
||||
def create_refresh_token(subject: str) -> str:
|
||||
"""生成 JWT refresh token。"""
|
||||
expire = datetime.now(timezone.utc) + timedelta(
|
||||
days=settings.jwt_refresh_token_ttl_days
|
||||
)
|
||||
payload = {"sub": subject, "exp": expire, "type": "refresh"}
|
||||
return jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)
|
||||
|
||||
|
||||
def decode_token(token: str) -> dict[str, Any]:
|
||||
"""解码 JWT token。"""
|
||||
return jwt.decode(token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm])
|
||||
@@ -0,0 +1,9 @@
|
||||
"""跨数据库兼容的 JSON 类型。
|
||||
|
||||
在 PostgreSQL 上使用 JSONB,在其他数据库(如 SQLite)上使用 JSON。
|
||||
"""
|
||||
|
||||
from sqlalchemy import JSON
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
JSONBType = JSON().with_variant(JSONB(), "postgresql")
|
||||
@@ -0,0 +1,171 @@
|
||||
"""FastAPI 应用入口。
|
||||
|
||||
注册中间件、路由、异常处理。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.routers.auth import router as auth_router
|
||||
from app.routers.companies import router as companies_router
|
||||
from app.routers.copilot import router as copilot_router
|
||||
from app.routers.dashboard import router as dashboard_router
|
||||
from app.routers.reports import router as reports_router
|
||||
from app.routers.reports_export import router as reports_export_router
|
||||
from app.routers.risks import router as risks_router
|
||||
|
||||
# Phase 2 路由
|
||||
from app.routers.financial import router as financial_router
|
||||
from app.routers.agreements import router as agreements_router
|
||||
from app.routers.board import router as board_router
|
||||
from app.routers.weak_signals import router as weak_signals_router
|
||||
from app.routers.decision_sentinels import router as decision_sentinels_router
|
||||
from app.routers.events import router as events_router, router_inquiries as inquiries_router
|
||||
from app.routers.profiles import router as profiles_router
|
||||
from app.routers.admin import router as admin_router
|
||||
from app.routers.founder import router as founder_router
|
||||
|
||||
# Phase 3 路由
|
||||
from app.routers.synergies import router as synergies_router
|
||||
from app.routers.innovation import router as innovation_router
|
||||
from app.routers.talents import router as talents_router
|
||||
from app.routers.customer_plans import router as customer_plans_router
|
||||
from app.routers.okrs import router as okrs_router
|
||||
from app.routers.nudges import router as nudges_router
|
||||
from app.routers.peer_circles import router as peer_circles_router
|
||||
from app.routers.product_diagnostics import router as product_diagnostics_router
|
||||
from app.routers.milestones import router as milestones_router
|
||||
from app.routers.advanced_analysis import router as advanced_analysis_router
|
||||
from app.routers.tasks import router_tasks as tasks_router, router_comments as comments_router
|
||||
from app.routers.customer_success import router as customer_success_router
|
||||
|
||||
# Phase 4 路由
|
||||
from app.routers.alpha import router as alpha_router
|
||||
from app.routers.exit_predictions import router as exit_predictions_router
|
||||
from app.routers.portfolio import router as portfolio_router
|
||||
from app.routers.digital_twins import router as digital_twins_router
|
||||
from app.routers.knowledge_graph import router as knowledge_graph_router
|
||||
from app.routers.aars import router as aars_router
|
||||
from app.routers.pre_mortems import router_pre_mortem as pre_mortems_router, router_red_team as red_teams_router
|
||||
from app.routers.agent_executions import router as agent_executions_router
|
||||
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 — 初始化预设评价模板
|
||||
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
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="AIPortPilot",
|
||||
description="AI+ Portfolio Operating System — 投后管理与组合协同平台",
|
||||
version="0.1.0",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
# CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:3000"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def trace_id_middleware(request: Request, call_next):
|
||||
"""为每个请求注入 trace_id。"""
|
||||
trace_id = request.headers.get("X-Trace-Id", str(uuid.uuid4()))
|
||||
request.state.trace_id = trace_id
|
||||
response = await call_next(request)
|
||||
response.headers["X-Trace-Id"] = trace_id
|
||||
return response
|
||||
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
"""全局异常处理。"""
|
||||
trace_id = getattr(request.state, "trace_id", str(uuid.uuid4()))
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content=error(code=-1, message="内部服务器错误"),
|
||||
headers={"X-Trace-Id": trace_id},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查端点。"""
|
||||
return {"status": "ok", "service": "aiportpilot-backend", "version": "0.1.0"}
|
||||
|
||||
|
||||
app.include_router(auth_router, prefix="/api/v1")
|
||||
app.include_router(companies_router, prefix="/api/v1")
|
||||
app.include_router(reports_router, prefix="/api/v1")
|
||||
app.include_router(dashboard_router, prefix="/api/v1")
|
||||
app.include_router(risks_router, prefix="/api/v1")
|
||||
app.include_router(copilot_router, prefix="/api/v1")
|
||||
app.include_router(reports_export_router, prefix="/api/v1")
|
||||
|
||||
# Phase 2
|
||||
app.include_router(financial_router, prefix="/api/v1")
|
||||
app.include_router(agreements_router, prefix="/api/v1")
|
||||
app.include_router(board_router, prefix="/api/v1")
|
||||
app.include_router(weak_signals_router, prefix="/api/v1")
|
||||
app.include_router(decision_sentinels_router, prefix="/api/v1")
|
||||
app.include_router(events_router, prefix="/api/v1")
|
||||
app.include_router(inquiries_router, prefix="/api/v1")
|
||||
app.include_router(profiles_router, prefix="/api/v1")
|
||||
app.include_router(admin_router, prefix="/api/v1")
|
||||
app.include_router(founder_router, prefix="/api/v1")
|
||||
|
||||
# Phase 3
|
||||
app.include_router(synergies_router, prefix="/api/v1")
|
||||
app.include_router(innovation_router, prefix="/api/v1")
|
||||
app.include_router(talents_router, prefix="/api/v1")
|
||||
app.include_router(customer_plans_router, prefix="/api/v1")
|
||||
app.include_router(okrs_router, prefix="/api/v1")
|
||||
app.include_router(nudges_router, prefix="/api/v1")
|
||||
app.include_router(peer_circles_router, prefix="/api/v1")
|
||||
app.include_router(product_diagnostics_router, prefix="/api/v1")
|
||||
app.include_router(milestones_router, prefix="/api/v1")
|
||||
app.include_router(advanced_analysis_router, prefix="/api/v1")
|
||||
app.include_router(tasks_router, prefix="/api/v1")
|
||||
app.include_router(comments_router, prefix="/api/v1")
|
||||
app.include_router(customer_success_router, prefix="/api/v1")
|
||||
|
||||
# Phase 4
|
||||
app.include_router(alpha_router, prefix="/api/v1")
|
||||
app.include_router(exit_predictions_router, prefix="/api/v1")
|
||||
app.include_router(portfolio_router, prefix="/api/v1")
|
||||
app.include_router(digital_twins_router, prefix="/api/v1")
|
||||
app.include_router(knowledge_graph_router, prefix="/api/v1")
|
||||
app.include_router(aars_router, prefix="/api/v1")
|
||||
app.include_router(pre_mortems_router, prefix="/api/v1")
|
||||
app.include_router(red_teams_router, prefix="/api/v1")
|
||||
app.include_router(agent_executions_router, prefix="/api/v1")
|
||||
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")
|
||||
@@ -0,0 +1,90 @@
|
||||
"""数据模型模块。
|
||||
|
||||
导入所有模型以便 Alembic 自动发现。
|
||||
"""
|
||||
|
||||
from app.models.aar import AARRecord
|
||||
from app.models.agent_execution import AgentExecution
|
||||
from app.models.agreement import InvestmentAgreement
|
||||
from app.models.audit import AuditLog
|
||||
from app.models.board import BoardMeeting
|
||||
from app.models.company import Company
|
||||
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
|
||||
from app.models.intervention import InterventionEvent, InterventionResult
|
||||
from app.models.knowledge import KnowledgeChunk
|
||||
from app.models.knowledge_graph import KnowledgeNode
|
||||
from app.models.major_event import MajorEvent
|
||||
from app.models.milestone import MilestoneTree
|
||||
from app.models.nudge import NudgeRecord
|
||||
from app.models.okr import OKR
|
||||
from app.models.peer_circle import PeerLearningCircle
|
||||
from app.models.portfolio_simulation import MonteCarloSimulation, PortfolioRebalancing
|
||||
from app.models.pre_mortem import PreMortemRecord, RedTeamRecord
|
||||
from app.models.product_diagnostic import ProductDiagnostic
|
||||
from app.models.profile import FirmProfile, FundProfile, ManagerProfile
|
||||
from app.models.report import MonthlyReport
|
||||
from app.models.risk import RiskEvent
|
||||
from app.models.synergy import SynergyOpportunity
|
||||
from app.models.talent import TalentProfile, TeamMember
|
||||
from app.models.task import Comment, Task
|
||||
from app.models.tenant import Tenant
|
||||
from app.models.user import User
|
||||
from app.models.weak_signal import WeakSignal
|
||||
|
||||
__all__ = [
|
||||
"AARRecord",
|
||||
"AgentExecution",
|
||||
"AuditLog",
|
||||
"BoardMeeting",
|
||||
"Comment",
|
||||
"Company",
|
||||
"CustomerAcquisitionPlan",
|
||||
"DataSource",
|
||||
"DecisionSentinel",
|
||||
"DigitalTwinModel",
|
||||
"EvaluationTemplate",
|
||||
"ExitPrediction",
|
||||
"FinancialData",
|
||||
"Fund",
|
||||
"CompanyFundLink",
|
||||
"FirmProfile",
|
||||
"FundProfile",
|
||||
"HealthScore",
|
||||
"Hypothesis",
|
||||
"InquiryList",
|
||||
"InterventionEvent",
|
||||
"InterventionResult",
|
||||
"InvestmentAgreement",
|
||||
"KnowledgeChunk",
|
||||
"KnowledgeNode",
|
||||
"MajorEvent",
|
||||
"ManagerProfile",
|
||||
"MilestoneTree",
|
||||
"MonteCarloSimulation",
|
||||
"MonthlyReport",
|
||||
"NudgeRecord",
|
||||
"OKR",
|
||||
"PeerLearningCircle",
|
||||
"PortfolioRebalancing",
|
||||
"PreMortemRecord",
|
||||
"ProductDiagnostic",
|
||||
"RedTeamRecord",
|
||||
"RiskEvent",
|
||||
"SynergyOpportunity",
|
||||
"TalentProfile",
|
||||
"Task",
|
||||
"TeamMember",
|
||||
"Tenant",
|
||||
"User",
|
||||
"WeakSignal",
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
"""AAR 系统化复盘模型。"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class AARRecord(Base):
|
||||
"""AAR 复盘记录。"""
|
||||
|
||||
__tablename__ = "aar_records"
|
||||
|
||||
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)
|
||||
trigger_event: Mapped[str] = mapped_column(String(200), nullable=False, comment="触发事件")
|
||||
original_plan: Mapped[str | None] = mapped_column(Text, nullable=True, comment="原计划")
|
||||
actual_result: Mapped[str | None] = mapped_column(Text, nullable=True, comment="实际结果")
|
||||
gap_analysis: Mapped[str | None] = mapped_column(Text, nullable=True, comment="差异分析")
|
||||
lessons: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="五问复盘结论")
|
||||
improvements: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="改进措施 + 执行追踪")
|
||||
knowledge_graph_ref: Mapped[str | None] = mapped_column(String(36), nullable=True, comment="知识图谱节点 ID")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Agent 执行记录模型。
|
||||
|
||||
L1-L4 分级自治执行。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class AgentExecution(Base):
|
||||
"""Agent 执行记录。"""
|
||||
|
||||
__tablename__ = "agent_executions"
|
||||
|
||||
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)
|
||||
agent_name: Mapped[str] = mapped_column(String(100), nullable=False, comment="Agent 名称")
|
||||
autonomy_level: Mapped[str] = mapped_column(String(10), nullable=False, comment="L1/L2/L3/L4")
|
||||
input_summary: Mapped[str | None] = mapped_column(Text, nullable=True, comment="输入摘要")
|
||||
output_summary: Mapped[str | None] = mapped_column(Text, nullable=True, comment="输出摘要")
|
||||
output_detail: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="输出详情")
|
||||
review_status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending", comment="pending/approved/rejected/auto_approved")
|
||||
reviewer_id: Mapped[str | None] = mapped_column(String(36), ForeignKey("users.id"), nullable=True)
|
||||
reviewed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
model_version: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
duration_ms: Mapped[int | None] = mapped_column(nullable=True, comment="执行耗时")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
"""投资协议模型。
|
||||
|
||||
协议条款提取与持续监控。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class InvestmentAgreement(Base):
|
||||
"""投资协议。"""
|
||||
|
||||
__tablename__ = "investment_agreements"
|
||||
|
||||
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)
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False, comment="协议名称")
|
||||
signed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, comment="签署日期")
|
||||
file_url: Mapped[str | None] = mapped_column(String(500), nullable=True, comment="文件 URL")
|
||||
key_clauses: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="关键条款 JSON")
|
||||
monitoring_rules: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="监控规则")
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="active", comment="active/expired/terminated")
|
||||
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,31 @@
|
||||
"""审计日志模型。
|
||||
|
||||
全链路操作记录,保留 ≥ 6 月。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class AuditLog(Base):
|
||||
"""审计日志。"""
|
||||
|
||||
__tablename__ = "audit_logs"
|
||||
|
||||
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)
|
||||
user_id: Mapped[str | None] = mapped_column(String(36), ForeignKey("users.id"), nullable=True)
|
||||
action: Mapped[str] = mapped_column(String(100), nullable=False, comment="操作类型:login/view/create/update/delete/export/ai_call")
|
||||
resource_type: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="资源类型")
|
||||
resource_id: Mapped[str | None] = mapped_column(String(36), nullable=True, comment="资源 ID")
|
||||
detail_json: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="操作详情")
|
||||
ip: Mapped[str | None] = mapped_column(String(45), nullable=True, comment="IP 地址")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
"""董事会会议模型。
|
||||
|
||||
议程、纪要、决议追踪。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class BoardMeeting(Base):
|
||||
"""董事会会议。"""
|
||||
|
||||
__tablename__ = "board_meetings"
|
||||
|
||||
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)
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False, comment="会议主题")
|
||||
meeting_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, comment="会议时间")
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="scheduled", comment="scheduled/in_progress/completed")
|
||||
agenda: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="议程列表")
|
||||
materials_summary: Mapped[str | None] = mapped_column(Text, nullable=True, comment="AI 会前材料摘要")
|
||||
minutes: Mapped[str | None] = mapped_column(Text, nullable=True, comment="会议纪要")
|
||||
resolutions: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="决议列表 — 含状态追踪")
|
||||
questions: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="AI 提问清单")
|
||||
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,39 @@
|
||||
"""企业模型。
|
||||
|
||||
被投企业档案,包含基本信息、业务描述、投资关系等。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class Company(Base):
|
||||
"""被投企业。"""
|
||||
|
||||
__tablename__ = "companies"
|
||||
|
||||
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="企业名称")
|
||||
industry: Mapped[str | None] = mapped_column(String(100), nullable=True, comment="行业")
|
||||
stage: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="融资阶段:seed/a/b/c/ipo")
|
||||
logo_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True, comment="业务描述")
|
||||
founded_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
total_funding: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="累计融资额")
|
||||
website: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
extra_json: Mapped[dict | None] = mapped_column(JSONBType, 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,38 @@
|
||||
"""客户获取计划模型。
|
||||
|
||||
AI 客户增长引擎 — LP 资源匹配 + 客户获取方案。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class CustomerAcquisitionPlan(Base):
|
||||
"""客户获取计划。"""
|
||||
|
||||
__tablename__ = "customer_acquisition_plans"
|
||||
|
||||
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)
|
||||
target_customer: Mapped[str | None] = mapped_column(Text, nullable=True, comment="目标客户画像")
|
||||
entry_angle: Mapped[str | None] = mapped_column(Text, nullable=True, comment="切入角度")
|
||||
decision_chain: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="决策链分析")
|
||||
pricing_strategy: Mapped[str | None] = mapped_column(Text, nullable=True, comment="定价策略")
|
||||
competitive_analysis: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="竞争分析")
|
||||
lp_resources: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="可利用 LP 资源")
|
||||
execution_status: Mapped[str] = mapped_column(String(20), nullable=False, default="planned", comment="planned/executing/completed/failed")
|
||||
result: Mapped[dict | None] = mapped_column(JSONBType, 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,35 @@
|
||||
"""数据源模型。"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class DataSource(Base):
|
||||
"""外部数据源配置。"""
|
||||
|
||||
__tablename__ = "data_sources"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
company_id: Mapped[str | None] = mapped_column(String(36), ForeignKey("companies.id"), nullable=True, index=True)
|
||||
tenant_id: Mapped[str] = mapped_column(String(36), ForeignKey("tenants.id"), nullable=False, index=True)
|
||||
source_type: Mapped[str] = mapped_column(String(50), nullable=False, comment="crunchbase/business_registry/github/custom")
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
api_endpoint: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
api_key_encrypted: Mapped[str | None] = mapped_column(Text, nullable=True, comment="加密后的 API Key")
|
||||
config: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="配置参数")
|
||||
last_synced_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="inactive", comment="active/inactive/error")
|
||||
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,39 @@
|
||||
"""决策前哨模型。
|
||||
|
||||
识别企业关键决策岔路口,AI 提前生成场景分析。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class DecisionSentinel(Base):
|
||||
"""决策前哨。"""
|
||||
|
||||
__tablename__ = "decision_sentinels"
|
||||
|
||||
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)
|
||||
decision_type: Mapped[str] = mapped_column(String(50), nullable=False, comment="pivot/hiring/funding/product/org")
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False, comment="决策标题")
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
signals: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="触发信号列表")
|
||||
scenarios: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="场景分析 — A 路线 vs B 路线")
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="identified", comment="identified/analyzed/acted/dismissed")
|
||||
identified_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
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,31 @@
|
||||
"""数字孪生模型。"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class DigitalTwinModel(Base):
|
||||
"""数字孪生。"""
|
||||
|
||||
__tablename__ = "digital_twins"
|
||||
|
||||
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)
|
||||
model_params: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="模型参数")
|
||||
scenarios: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="模拟场景列表")
|
||||
accuracy_score: Mapped[float | None] = mapped_column(Float, nullable=True, comment="精度评分(0-1)")
|
||||
last_calibrated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
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,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,29 @@
|
||||
"""退出预测模型。"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class ExitPrediction(Base):
|
||||
"""退出时机预测。"""
|
||||
|
||||
__tablename__ = "exit_predictions"
|
||||
|
||||
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)
|
||||
exit_path: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="ipo/acquisition/secondary/merger")
|
||||
timing_window: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="时机窗口 — 起止时间")
|
||||
expected_return: Mapped[float | None] = mapped_column(Float, nullable=True, comment="期望收益率")
|
||||
hold_return: Mapped[float | None] = mapped_column(Float, nullable=True, comment="继续持有预期收益率")
|
||||
confidence: Mapped[float | None] = mapped_column(Float, nullable=True, comment="置信度")
|
||||
signals: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="退出信号")
|
||||
recommendation: Mapped[str | None] = mapped_column(Text, nullable=True, comment="退出建议")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
"""财务数据模型。
|
||||
|
||||
资产负债表、利润表、现金流量表、科目余额。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class FinancialData(Base):
|
||||
"""财务数据。"""
|
||||
|
||||
__tablename__ = "financial_data"
|
||||
|
||||
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)
|
||||
period_year: Mapped[int] = mapped_column(Integer, nullable=False, comment="年份")
|
||||
period_month: Mapped[int] = mapped_column(Integer, nullable=False, comment="月份")
|
||||
statement_type: Mapped[str] = mapped_column(String(50), nullable=False, comment="balance_sheet/income/cash_flow")
|
||||
data_json: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="财务数据 JSON")
|
||||
source: Mapped[str | None] = mapped_column(String(100), nullable=True, comment="数据来源")
|
||||
credibility_score: Mapped[float | None] = mapped_column(Float, nullable=True, comment="可信度评分(0-100)")
|
||||
validation_result: Mapped[dict | None] = mapped_column(JSONBType, 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)
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
"""健康度评分模型。
|
||||
|
||||
多维度评分:财务、经营、AI+ 商业化、AI+ 成本(基础 4 维度)。
|
||||
T2.9 扩展:组织人才、产品技术、市场竞争、治理合规、融资资本(9 维度)。
|
||||
T3.11 扩展:协同赋能、AI 模型产品、数据合规、团队技术、客户成功(14 维度)。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, String
|
||||
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):
|
||||
"""健康度评分。"""
|
||||
|
||||
__tablename__ = "health_scores"
|
||||
|
||||
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)
|
||||
total_score: Mapped[float] = mapped_column(Float, nullable=False, comment="总分(0-100)")
|
||||
# 基础 4 维度
|
||||
financial_score: Mapped[float | None] = mapped_column(Float, nullable=True, comment="财务健康度")
|
||||
operational_score: Mapped[float | None] = mapped_column(Float, nullable=True, comment="经营健康度")
|
||||
ai_commercial_score: Mapped[float | None] = mapped_column(Float, nullable=True, comment="AI+ 商业化健康度")
|
||||
ai_cost_score: Mapped[float | None] = mapped_column(Float, nullable=True, comment="AI+ 成本健康度")
|
||||
# T2.9 扩展 5 维度
|
||||
org_talent_score: Mapped[float | None] = mapped_column(Float, nullable=True, comment="组织人才健康度")
|
||||
product_tech_score: Mapped[float | None] = mapped_column(Float, nullable=True, comment="产品技术健康度")
|
||||
market_compete_score: Mapped[float | None] = mapped_column(Float, nullable=True, comment="市场竞争健康度")
|
||||
governance_score: Mapped[float | None] = mapped_column(Float, nullable=True, comment="治理合规健康度")
|
||||
financing_score: Mapped[float | None] = mapped_column(Float, nullable=True, comment="融资资本健康度")
|
||||
# T3.11 扩展 5 维度
|
||||
synergy_score: Mapped[float | None] = mapped_column(Float, nullable=True, comment="协同赋能健康度")
|
||||
ai_model_product_score: Mapped[float | None] = mapped_column(Float, nullable=True, comment="AI 模型产品健康度")
|
||||
data_compliance_score: Mapped[float | None] = mapped_column(Float, nullable=True, comment="数据合规健康度")
|
||||
team_tech_score: Mapped[float | None] = mapped_column(Float, nullable=True, comment="团队技术健康度")
|
||||
customer_success_score: Mapped[float | None] = mapped_column(Float, nullable=True, comment="客户成功健康度")
|
||||
# 元数据
|
||||
trend: Mapped[str | None] = mapped_column(String(20), nullable=True, comment="趋势:up/stable/down")
|
||||
evidence_json: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="评分依据")
|
||||
recommendations_json: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="建议动作")
|
||||
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,35 @@
|
||||
"""BML 认知追踪模型。
|
||||
|
||||
假设/实验/数据/结论 — Build-Measure-Learn 循环。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class Hypothesis(Base):
|
||||
"""BML 认知追踪 — 假设记录。"""
|
||||
|
||||
__tablename__ = "hypotheses"
|
||||
|
||||
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)
|
||||
hypothesis: Mapped[str] = mapped_column(Text, nullable=False, comment="假设")
|
||||
experiment: Mapped[str | None] = mapped_column(Text, nullable=True, comment="验证实验")
|
||||
data: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="实验数据")
|
||||
conclusion: Mapped[str | None] = mapped_column(Text, nullable=True, comment="结论")
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="building", comment="building/measuring/learning/validated/invalidated")
|
||||
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,37 @@
|
||||
"""追问清单模型。
|
||||
|
||||
AI 根据月报数据生成补充问题,企业可回复。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class InquiryList(Base):
|
||||
"""追问清单。"""
|
||||
|
||||
__tablename__ = "inquiry_lists"
|
||||
|
||||
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)
|
||||
report_id: Mapped[str | None] = mapped_column(String(36), ForeignKey("monthly_reports.id"), nullable=True)
|
||||
questions: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="问题列表 — 含问题和回答")
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="sent", comment="sent/answered/closed")
|
||||
sent_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
answered_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
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,49 @@
|
||||
"""干预事件与结果模型。
|
||||
|
||||
投后管理 Alpha 归因 — 干预事件 → 指标变化 → 估值影响 → 回报贡献。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class InterventionEvent(Base):
|
||||
"""干预事件。"""
|
||||
|
||||
__tablename__ = "intervention_events"
|
||||
|
||||
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)
|
||||
intervention_type: Mapped[str] = mapped_column(String(50), nullable=False, comment="recruitment/customer_intro/strategy/governance/crisis/funding")
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
executed_by: Mapped[str | None] = mapped_column(String(36), ForeignKey("users.id"), nullable=True)
|
||||
executed_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
|
||||
class InterventionResult(Base):
|
||||
"""干预结果。"""
|
||||
|
||||
__tablename__ = "intervention_results"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
intervention_id: Mapped[str] = mapped_column(String(36), ForeignKey("intervention_events.id"), nullable=False, index=True)
|
||||
metric_changes: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="指标变化")
|
||||
valuation_impact: Mapped[float | None] = mapped_column(Float, nullable=True, comment="估值影响")
|
||||
return_contribution: Mapped[float | None] = mapped_column(Float, nullable=True, comment="回报贡献")
|
||||
alpha_score: Mapped[float | None] = mapped_column(Float, nullable=True, comment="Alpha 归因评分")
|
||||
evidence: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="证据链")
|
||||
measured_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
"""RAG 知识库模型。"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import JSON, DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class KnowledgeChunk(Base):
|
||||
"""知识库分块 — 向量化的月报/报告片段。"""
|
||||
|
||||
__tablename__ = "knowledge_chunks"
|
||||
|
||||
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)
|
||||
source_type: Mapped[str] = mapped_column(String(50), nullable=False, comment="report/agreement/board/aar/knowledge_graph")
|
||||
source_id: Mapped[str | None] = mapped_column(String(36), nullable=True, comment="来源记录 ID")
|
||||
company_id: Mapped[str | None] = mapped_column(String(36), ForeignKey("companies.id"), nullable=True, index=True)
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False, comment="文本内容")
|
||||
embedding: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="向量嵌入")
|
||||
metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="元数据")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
"""知识图谱模型。
|
||||
|
||||
企业特征 + 管理动作 + 环境上下文 → 结果 → 回报影响。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import JSON, DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class KnowledgeNode(Base):
|
||||
"""知识图谱节点。"""
|
||||
|
||||
__tablename__ = "knowledge_nodes"
|
||||
|
||||
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)
|
||||
entity_type: Mapped[str] = mapped_column(String(50), nullable=False, comment="company/action/context/result/return")
|
||||
entity_id: Mapped[str | None] = mapped_column(String(36), nullable=True, comment="关联实体 ID")
|
||||
attributes: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="实体属性")
|
||||
relations: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="关系列表 — target_id + relation_type")
|
||||
embedding: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="向量嵌入")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
"""重大事项模型。
|
||||
|
||||
AI 从月报/弱信号中自动识别重大事项。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class MajorEvent(Base):
|
||||
"""重大事项。"""
|
||||
|
||||
__tablename__ = "major_events"
|
||||
|
||||
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)
|
||||
event_type: Mapped[str] = mapped_column(String(50), nullable=False, comment="funding/personnel/product/legal/market/org")
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
severity: Mapped[str] = mapped_column(String(20), nullable=False, default="medium", comment="low/medium/high/critical")
|
||||
source: Mapped[str | None] = mapped_column(String(100), nullable=True, comment="来源:monthly_report/weak_signal/manual")
|
||||
source_ref: Mapped[str | None] = mapped_column(String(36), nullable=True, comment="来源记录 ID")
|
||||
evidence: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="证据链")
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="identified", comment="identified/confirmed/addressed")
|
||||
occurred_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
"""里程碑树模型。
|
||||
|
||||
分支路径管理 + 环境变化时 AI 建议路径切换。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class MilestoneTree(Base):
|
||||
"""里程碑树。"""
|
||||
|
||||
__tablename__ = "milestone_trees"
|
||||
|
||||
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)
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False, comment="里程碑名称")
|
||||
parent_id: Mapped[str | None] = mapped_column(String(36), ForeignKey("milestone_trees.id"), nullable=True, comment="父节点")
|
||||
is_current: Mapped[bool] = mapped_column(default=False, comment="是否当前路径")
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="planned", comment="planned/in_progress/completed/abandoned")
|
||||
target_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
actual_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
ai_analysis: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="AI 路径分析")
|
||||
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,31 @@
|
||||
"""行为助推记录模型。
|
||||
|
||||
时机判断 + 策略选择 + 效果追踪。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class NudgeRecord(Base):
|
||||
"""行为助推记录。"""
|
||||
|
||||
__tablename__ = "nudge_records"
|
||||
|
||||
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)
|
||||
nudge_type: Mapped[str] = mapped_column(String(50), nullable=False, comment="anchoring/loss_aversion/social_proof/default/timing")
|
||||
context: Mapped[str | None] = mapped_column(Text, nullable=True, comment="助推上下文")
|
||||
message: Mapped[str] = mapped_column(Text, nullable=False, comment="助推内容")
|
||||
target_user_id: Mapped[str | None] = mapped_column(String(36), ForeignKey("users.id"), nullable=True)
|
||||
accepted: Mapped[bool | None] = mapped_column(nullable=True, comment="是否接受")
|
||||
effect_result: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="效果追踪")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
"""OKR 模型。
|
||||
|
||||
投资人与创始人共同制定 OKR + AI 对齐度评分。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class OKR(Base):
|
||||
"""OKR。"""
|
||||
|
||||
__tablename__ = "okrs"
|
||||
|
||||
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)
|
||||
quarter: Mapped[str] = mapped_column(String(10), nullable=False, comment="如 2025-Q1")
|
||||
objective: Mapped[str] = mapped_column(Text, nullable=False, comment="目标")
|
||||
key_results: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="关键结果列表 — 含进度")
|
||||
alignment_score: Mapped[float | None] = mapped_column(nullable=True, comment="对齐度评分(0-100)")
|
||||
deviation_alerts: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="偏差预警")
|
||||
review_notes: Mapped[str | None] = mapped_column(Text, nullable=True, comment="复盘记录")
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="active", comment="active/completed/archived")
|
||||
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,37 @@
|
||||
"""同行学习圈模型。
|
||||
|
||||
AI 匹配面临类似挑战的创始人,结构化讨论。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class PeerLearningCircle(Base):
|
||||
"""同行学习圈。"""
|
||||
|
||||
__tablename__ = "peer_learning_circles"
|
||||
|
||||
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)
|
||||
topic: Mapped[str] = mapped_column(String(200), nullable=False, comment="讨论话题")
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
members: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="成员列表 — 创始人 ID + 企业 ID")
|
||||
discussion_framework: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="结构化讨论框架")
|
||||
conclusions: Mapped[str | None] = mapped_column(Text, nullable=True, comment="讨论结论")
|
||||
action_commitments: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="行动承诺")
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="matching", comment="matching/active/completed")
|
||||
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,45 @@
|
||||
"""组合再平衡 + Monte Carlo 模拟模型。"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class PortfolioRebalancing(Base):
|
||||
"""组合再平衡建议。"""
|
||||
|
||||
__tablename__ = "portfolio_rebalancings"
|
||||
|
||||
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)
|
||||
marginal_returns: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="各企业边际回报率")
|
||||
reallocation_plan: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="再平衡方案")
|
||||
irr_impact: Mapped[float | None] = mapped_column(Float, nullable=True, comment="IRR 影响")
|
||||
dpi_impact: Mapped[float | None] = mapped_column(Float, nullable=True, comment="DPI 影响")
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="proposed", comment="proposed/approved/executed")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
|
||||
class MonteCarloSimulation(Base):
|
||||
"""Monte Carlo 模拟结果。"""
|
||||
|
||||
__tablename__ = "monte_carlo_simulations"
|
||||
|
||||
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)
|
||||
iterations: Mapped[int] = mapped_column(nullable=False, default=10000, comment="模拟次数")
|
||||
irr_distribution: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="IRR 概率分布")
|
||||
dpi_distribution: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="DPI 概率分布")
|
||||
percentile_p5: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
percentile_p50: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
percentile_p95: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Pre-mortem + Red Team 模型。"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class PreMortemRecord(Base):
|
||||
"""Pre-mortem 失败推演。"""
|
||||
|
||||
__tablename__ = "pre_mortem_records"
|
||||
|
||||
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)
|
||||
decision_context: Mapped[str | None] = mapped_column(Text, nullable=True, comment="决策上下文")
|
||||
failure_paths: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="失败路径列表")
|
||||
risk_checklist: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="风险清单")
|
||||
mitigations: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="缓解措施")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
|
||||
class RedTeamRecord(Base):
|
||||
"""Red Team 对抗分析。"""
|
||||
|
||||
__tablename__ = "red_team_records"
|
||||
|
||||
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)
|
||||
perspective: Mapped[str] = mapped_column(String(50), nullable=False, comment="competitor/pessimistic_investor/devils_advocate")
|
||||
analysis: Mapped[str | None] = mapped_column(Text, nullable=True, comment="对抗分析内容")
|
||||
vulnerabilities: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="发现的漏洞")
|
||||
counterarguments: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="反驳论点")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
"""产品竞争力诊断模型。
|
||||
|
||||
AI 体验产品 + 竞品对比 + 热力图。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class ProductDiagnostic(Base):
|
||||
"""产品竞争力诊断。"""
|
||||
|
||||
__tablename__ = "product_diagnostics"
|
||||
|
||||
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)
|
||||
product_name: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
||||
dimensions: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="竞争力维度评分")
|
||||
heatmap_data: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="热力图数据")
|
||||
competitors: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="竞品对比")
|
||||
roadmap_suggestions: Mapped[str | None] = mapped_column(Text, nullable=True, comment="路线图建议")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -0,0 +1,79 @@
|
||||
"""多主体画像模型。
|
||||
|
||||
投资机构、基金、投资经理画像。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class FirmProfile(Base):
|
||||
"""投资机构画像。"""
|
||||
|
||||
__tablename__ = "firm_profiles"
|
||||
|
||||
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="机构名称")
|
||||
focus_areas: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="投资领域")
|
||||
stage_preference: Mapped[str | None] = mapped_column(String(200), nullable=True, comment="阶段偏好")
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
extra_json: Mapped[dict | None] = mapped_column(JSONBType, nullable=True)
|
||||
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),
|
||||
)
|
||||
|
||||
|
||||
class FundProfile(Base):
|
||||
"""基金画像。"""
|
||||
|
||||
__tablename__ = "fund_profiles"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
firm_id: Mapped[str] = mapped_column(String(36), ForeignKey("firm_profiles.id"), nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False, comment="基金名称")
|
||||
fund_size: Mapped[str | None] = mapped_column(String(100), nullable=True, comment="基金规模")
|
||||
vintage_year: Mapped[int | None] = mapped_column(nullable=True, comment="成立年份")
|
||||
strategy: Mapped[str | None] = mapped_column(Text, nullable=True, comment="投资策略")
|
||||
extra_json: Mapped[dict | None] = mapped_column(JSONBType, nullable=True)
|
||||
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),
|
||||
)
|
||||
|
||||
|
||||
class ManagerProfile(Base):
|
||||
"""投资经理画像。"""
|
||||
|
||||
__tablename__ = "manager_profiles"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
firm_id: Mapped[str] = mapped_column(String(36), ForeignKey("firm_profiles.id"), nullable=False, index=True)
|
||||
user_id: Mapped[str | None] = mapped_column(String(36), ForeignKey("users.id"), nullable=True)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False, comment="投资经理姓名")
|
||||
focus_areas: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="关注领域")
|
||||
portfolio_count: Mapped[int | None] = mapped_column(nullable=True, comment="在管企业数")
|
||||
extra_json: Mapped[dict | None] = mapped_column(JSONBType, nullable=True)
|
||||
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,44 @@
|
||||
"""月报模型。
|
||||
|
||||
被投企业按月提交的经营报告,支持 AI 解析。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class MonthlyReport(Base):
|
||||
"""月报。"""
|
||||
|
||||
__tablename__ = "monthly_reports"
|
||||
|
||||
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)
|
||||
period_year: Mapped[int] = mapped_column(Integer, nullable=False, comment="报告年份")
|
||||
period_month: Mapped[int] = mapped_column(Integer, nullable=False, comment="报告月份(1-12)")
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(50), nullable=False, default="draft",
|
||||
comment="状态:draft/submitted/ai_parsed/reviewed",
|
||||
)
|
||||
raw_content: Mapped[str | None] = mapped_column(Text, nullable=True, comment="原始内容")
|
||||
structured_data: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="结构化指标数据")
|
||||
ai_summary: Mapped[str | None] = mapped_column(Text, nullable=True, comment="AI 生成的摘要")
|
||||
ai_concerns: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="AI 关注点列表")
|
||||
submitted_by: Mapped[str | None] = mapped_column(String(36), ForeignKey("users.id"), nullable=True)
|
||||
submitted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
reviewed_by: Mapped[str | None] = mapped_column(String(36), ForeignKey("users.id"), nullable=True)
|
||||
reviewed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
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,43 @@
|
||||
"""风险事件模型。
|
||||
|
||||
指标越界自动预警 + 人工处理闭环。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class RiskEvent(Base):
|
||||
"""风险事件。"""
|
||||
|
||||
__tablename__ = "risk_events"
|
||||
|
||||
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)
|
||||
type: Mapped[str] = mapped_column(String(50), nullable=False, comment="风险类型:financial/operational/org/ai_specific")
|
||||
severity: Mapped[str] = mapped_column(String(20), nullable=False, default="medium", comment="严重程度:low/medium/high/critical")
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="open", comment="状态:open/assigned/in_progress/resolved/closed")
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
evidence_json: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="证据链")
|
||||
suggested_action: Mapped[str | None] = mapped_column(Text, nullable=True, comment="建议动作")
|
||||
assigned_to: Mapped[str | None] = mapped_column(String(36), ForeignKey("users.id"), nullable=True)
|
||||
due_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
identified_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
closed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
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,39 @@
|
||||
"""协同机会模型。
|
||||
|
||||
Portfolio 内部协同匹配与效果追踪。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class SynergyOpportunity(Base):
|
||||
"""协同机会。"""
|
||||
|
||||
__tablename__ = "synergy_opportunities"
|
||||
|
||||
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)
|
||||
type: Mapped[str] = mapped_column(String(50), nullable=False, comment="customer/talent/funding/supply_chain/tech")
|
||||
company_a_id: Mapped[str] = mapped_column(String(36), ForeignKey("companies.id"), nullable=False, index=True)
|
||||
company_b_id: Mapped[str | None] = mapped_column(String(36), ForeignKey("companies.id"), nullable=True)
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
match_reason: Mapped[str | None] = mapped_column(Text, nullable=True, comment="AI 匹配理由")
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="discovered", comment="discovered/confirmed/authorized/executing/completed/declined")
|
||||
authorized: Mapped[bool] = mapped_column(default=False, comment="双方是否授权")
|
||||
effect_result: Mapped[dict | None] = mapped_column(JSONBType, 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,59 @@
|
||||
"""人才模型。
|
||||
|
||||
核心人才画像 + 团队成员 + 9-Box 矩阵 + 流动预测。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class TalentProfile(Base):
|
||||
"""人才画像。"""
|
||||
|
||||
__tablename__ = "talent_profiles"
|
||||
|
||||
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(100), nullable=False)
|
||||
current_role: Mapped[str | None] = mapped_column(String(200), nullable=True, comment="当前职位")
|
||||
current_company: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
||||
skills: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="技能标签")
|
||||
experience_years: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
performance_rating: Mapped[float | None] = mapped_column(nullable=True, comment="绩效评分(1-5)")
|
||||
potential_rating: Mapped[float | None] = mapped_column(nullable=True, comment="潜力评分(1-5)")
|
||||
nine_box: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="9-Box 象限")
|
||||
flow_prediction: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="流动预测")
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="active", comment="active/flowed/inactive")
|
||||
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),
|
||||
)
|
||||
|
||||
|
||||
class TeamMember(Base):
|
||||
"""团队成员。"""
|
||||
|
||||
__tablename__ = "team_members"
|
||||
|
||||
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)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
role: Mapped[str | None] = mapped_column(String(200), nullable=True, comment="职位")
|
||||
is_key_person: Mapped[bool] = mapped_column(default=False, comment="是否核心人员")
|
||||
joined_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
left_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
stability_score: Mapped[float | None] = mapped_column(nullable=True, comment="稳定性评分(0-1)")
|
||||
extra_json: Mapped[dict | None] = mapped_column(JSONBType, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -0,0 +1,57 @@
|
||||
"""任务与评论模型。
|
||||
|
||||
投后任务管理 + 评论协作。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class Task(Base):
|
||||
"""投后任务。"""
|
||||
|
||||
__tablename__ = "tasks"
|
||||
|
||||
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)
|
||||
company_id: Mapped[str | None] = mapped_column(String(36), ForeignKey("companies.id"), nullable=True, index=True)
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="todo", comment="todo/in_progress/done/cancelled")
|
||||
priority: Mapped[str] = mapped_column(String(20), nullable=False, default="medium", comment="low/medium/high/urgent")
|
||||
assigned_to: Mapped[str | None] = mapped_column(String(36), ForeignKey("users.id"), nullable=True)
|
||||
source_type: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="来源:risk/report/synergy/manual")
|
||||
source_ref: Mapped[str | None] = mapped_column(String(36), nullable=True, comment="来源记录 ID")
|
||||
due_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
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),
|
||||
)
|
||||
|
||||
|
||||
class Comment(Base):
|
||||
"""评论。"""
|
||||
|
||||
__tablename__ = "comments"
|
||||
|
||||
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)
|
||||
target_type: Mapped[str] = mapped_column(String(50), nullable=False, comment="report/risk/company/task")
|
||||
target_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True, comment="目标记录 ID")
|
||||
user_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False)
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
parent_id: Mapped[str | None] = mapped_column(String(36), ForeignKey("comments.id"), nullable=True, comment="父评论 ID")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
"""租户模型。
|
||||
|
||||
投资机构和被投企业都属于某个租户。多租户隔离的基础。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class Tenant(Base):
|
||||
"""租户(投资机构)。"""
|
||||
|
||||
__tablename__ = "tenants"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False, comment="租户名称")
|
||||
type: Mapped[str] = mapped_column(String(50), nullable=False, default="vc", comment="租户类型:vc/cvc/gov/holdings")
|
||||
config_json: Mapped[dict | None] = mapped_column(JSONBType, 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,38 @@
|
||||
"""用户模型。
|
||||
|
||||
支持多种角色:GP、投资经理、投后负责人、创始人、管理员等。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""用户。"""
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
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)
|
||||
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
|
||||
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
role: Mapped[str] = mapped_column(
|
||||
String(50), nullable=False, default="investor",
|
||||
comment="角色:gp/partner/post_invest_lead/investor/founder/admin",
|
||||
)
|
||||
phone: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
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,36 @@
|
||||
"""弱信号模型。
|
||||
|
||||
技术/情绪/组织/市场四类弱信号采集与关联。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.types import JSONBType
|
||||
|
||||
|
||||
class WeakSignal(Base):
|
||||
"""弱信号。"""
|
||||
|
||||
__tablename__ = "weak_signals"
|
||||
|
||||
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)
|
||||
signal_type: Mapped[str] = mapped_column(String(50), nullable=False, comment="technical/sentiment/org/market")
|
||||
source: Mapped[str | None] = mapped_column(String(200), nullable=True, comment="信号来源")
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False, comment="信号内容")
|
||||
confidence: Mapped[float] = mapped_column(Float, nullable=False, default=0.5, comment="置信度(0-1)")
|
||||
correlation_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True, comment="关联组 ID")
|
||||
correlation_result: Mapped[dict | None] = mapped_column(JSONBType, nullable=True, comment="关联分析结果")
|
||||
risk_probability: Mapped[float | None] = mapped_column(Float, nullable=True, comment="风险概率(0-1)")
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="new", comment="new/correlated/alerted/dismissed")
|
||||
detected_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""API 路由模块。"""
|
||||
@@ -0,0 +1,48 @@
|
||||
"""AAR 路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
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.aar import AARRecord
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.aar_agent import generate_aar
|
||||
|
||||
router = APIRouter(prefix="/aars", tags=["aars"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[list])
|
||||
async def list_aars(company_id: str | None = Query(default=None), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""获取 AAR 复盘列表。"""
|
||||
query = select(AARRecord)
|
||||
if company_id:
|
||||
query = query.where(AARRecord.company_id == company_id)
|
||||
result = await db.execute(query.order_by(AARRecord.created_at.desc()))
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"company_id": str(i.company_id),
|
||||
"trigger_event": i.trigger_event,
|
||||
"original_plan": i.original_plan,
|
||||
"actual_result": i.actual_result,
|
||||
"gap_analysis": i.gap_analysis,
|
||||
"lessons": i.lessons,
|
||||
"improvements": i.improvements,
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("/generate", response_model=ApiResponse[dict])
|
||||
async def generate_aar_report(req: dict, user: User = Depends(get_current_user)):
|
||||
"""AI 生成五问复盘。"""
|
||||
result = await generate_aar(
|
||||
req.get("trigger_event", ""),
|
||||
req.get("original_plan", ""),
|
||||
req.get("actual_result", ""),
|
||||
)
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Admin 管理后台路由 — 租户 CRUD + 用户管理 + 审计日志。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
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, require_role
|
||||
from app.models.audit import AuditLog
|
||||
from app.models.tenant import Tenant
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
|
||||
|
||||
@router.get("/overview", response_model=ApiResponse[dict])
|
||||
async def admin_overview(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_role("admin")),
|
||||
):
|
||||
"""系统概览。"""
|
||||
tenants_result = await db.execute(select(Tenant))
|
||||
tenants = tenants_result.scalars().all()
|
||||
|
||||
users_result = await db.execute(select(User))
|
||||
users = users_result.scalars().all()
|
||||
|
||||
return success(data={
|
||||
"tenant_count": len(tenants),
|
||||
"user_count": len(users),
|
||||
"tenants": [{"id": str(t.id), "name": t.name} for t in tenants],
|
||||
})
|
||||
|
||||
|
||||
@router.get("/tenants", response_model=ApiResponse[list])
|
||||
async def list_tenants(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_role("admin")),
|
||||
):
|
||||
"""租户管理列表。"""
|
||||
result = await db.execute(select(Tenant))
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{"id": str(i.id), "name": i.name, "created_at": i.created_at.isoformat() if i.created_at else None}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.get("/users", response_model=ApiResponse[list])
|
||||
async def list_users(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_role("admin")),
|
||||
):
|
||||
"""用户管理列表。"""
|
||||
result = await db.execute(select(User))
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{"id": str(i.id), "email": i.email, "name": i.name, "role": i.role, "tenant_id": str(i.tenant_id), "is_active": i.is_active}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.get("/audit-logs", response_model=ApiResponse[list])
|
||||
async def list_audit_logs(
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_role("admin")),
|
||||
):
|
||||
"""审计日志查看。"""
|
||||
offset = (page - 1) * page_size
|
||||
result = await db.execute(
|
||||
select(AuditLog).order_by(AuditLog.created_at.desc()).offset(offset).limit(page_size)
|
||||
)
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"user_id": str(i.user_id) if i.user_id else None,
|
||||
"action": i.action,
|
||||
"target_type": i.resource_type,
|
||||
"target_id": i.resource_id,
|
||||
"detail": i.detail_json,
|
||||
"created_at": i.created_at.isoformat() if i.created_at else None,
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
@@ -0,0 +1,24 @@
|
||||
"""高级分析路由 — 约束点 + BML + 鸿沟诊断。"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from app.core.dependencies import get_current_user
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.constraint_analyzer import identify_constraints
|
||||
from app.services.chasm_diagnostic import diagnose_chasm
|
||||
|
||||
router = APIRouter(prefix="/advanced-analysis", tags=["advanced-analysis"])
|
||||
|
||||
|
||||
@router.post("/constraints", response_model=ApiResponse[dict])
|
||||
async def analyze_constraints(req: dict, user: User = Depends(get_current_user)):
|
||||
"""TOC 约束点识别。"""
|
||||
result = await identify_constraints(req.get("company_data", ""))
|
||||
return success(data=result)
|
||||
|
||||
|
||||
@router.post("/chasm", response_model=ApiResponse[dict])
|
||||
async def analyze_chasm(req: dict, user: User = Depends(get_current_user)):
|
||||
"""鸿沟诊断。"""
|
||||
result = await diagnose_chasm(req.get("company_data", ""))
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Agent 执行记录路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
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.agent_execution import AgentExecution
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.agent_orchestrator import orchestrate_agent
|
||||
|
||||
router = APIRouter(prefix="/agent-executions", tags=["agent-executions"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[list])
|
||||
async def list_executions(
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取 Agent 执行记录列表。"""
|
||||
offset = (page - 1) * page_size
|
||||
result = await db.execute(
|
||||
select(AgentExecution)
|
||||
.where(AgentExecution.tenant_id == user.tenant_id)
|
||||
.order_by(AgentExecution.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(page_size)
|
||||
)
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"agent_name": i.agent_name,
|
||||
"autonomy_level": i.autonomy_level,
|
||||
"input_summary": i.input_summary,
|
||||
"output_summary": i.output_summary,
|
||||
"review_status": i.review_status,
|
||||
"reviewer_id": i.reviewer_id,
|
||||
"duration_ms": i.duration_ms,
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("/orchestrate", response_model=ApiResponse[dict])
|
||||
async def orchestrate(req: dict, user: User = Depends(get_current_user)):
|
||||
"""编排 Agent 执行。"""
|
||||
result = await orchestrate_agent(
|
||||
req.get("agent_name", ""),
|
||||
req.get("autonomy_level", "L1"),
|
||||
req.get("input_data", {}),
|
||||
)
|
||||
return success(data=result)
|
||||
|
||||
|
||||
@router.put("/{execution_id}/review", response_model=ApiResponse[dict])
|
||||
async def review_execution(
|
||||
execution_id: str,
|
||||
req: dict,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""审核 Agent 执行。"""
|
||||
result = await db.execute(
|
||||
select(AgentExecution).where(AgentExecution.id == execution_id, AgentExecution.tenant_id == user.tenant_id)
|
||||
)
|
||||
execution = result.scalar_one_or_none()
|
||||
if not execution:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=404, detail="执行记录不存在")
|
||||
execution.review_status = req.get("review_status", "approved")
|
||||
execution.reviewer_id = str(user.id)
|
||||
from datetime import datetime, timezone
|
||||
execution.reviewed_at = datetime.now(timezone.utc)
|
||||
await db.flush()
|
||||
return success(data={"id": str(execution.id), "review_status": execution.review_status}, message="审核完成")
|
||||
@@ -0,0 +1,91 @@
|
||||
"""投资协议路由:CRUD + 条款预警。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
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.agreement import InvestmentAgreement
|
||||
from app.models.company import Company
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.agreement_monitor import check_clause_triggers
|
||||
|
||||
router = APIRouter(prefix="/agreements", tags=["agreements"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[list])
|
||||
async def list_agreements(
|
||||
company_id: str | None = Query(default=None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取投资协议列表。"""
|
||||
query = (
|
||||
select(InvestmentAgreement)
|
||||
.join(Company, InvestmentAgreement.company_id == Company.id)
|
||||
.where(Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
if company_id:
|
||||
query = query.where(InvestmentAgreement.company_id == company_id)
|
||||
result = await db.execute(query.order_by(InvestmentAgreement.created_at.desc()))
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"company_id": str(i.company_id),
|
||||
"title": i.title,
|
||||
"signed_at": i.signed_at.isoformat() if i.signed_at else None,
|
||||
"key_clauses": i.key_clauses,
|
||||
"monitoring_rules": i.monitoring_rules,
|
||||
"status": i.status,
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("", response_model=ApiResponse[dict], status_code=status.HTTP_201_CREATED)
|
||||
async def create_agreement(
|
||||
req: dict,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""创建投资协议。"""
|
||||
company_result = await db.execute(
|
||||
select(Company).where(Company.id == req.get("company_id"), Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
if not company_result.scalar_one_or_none():
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="企业不存在")
|
||||
|
||||
agreement = InvestmentAgreement(
|
||||
company_id=req.get("company_id"),
|
||||
title=req.get("title"),
|
||||
signed_at=req.get("signed_at"),
|
||||
file_url=req.get("file_url"),
|
||||
key_clauses=req.get("key_clauses"),
|
||||
monitoring_rules=req.get("monitoring_rules"),
|
||||
)
|
||||
db.add(agreement)
|
||||
await db.flush()
|
||||
return success(data={"id": str(agreement.id)}, message="创建成功")
|
||||
|
||||
|
||||
@router.get("/{agreement_id}/alerts", response_model=ApiResponse[list])
|
||||
async def get_clause_alerts(
|
||||
agreement_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取协议条款预警。"""
|
||||
result = await db.execute(
|
||||
select(InvestmentAgreement)
|
||||
.join(Company, InvestmentAgreement.company_id == Company.id)
|
||||
.where(InvestmentAgreement.id == agreement_id, Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
agreement = result.scalar_one_or_none()
|
||||
if not agreement:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="协议不存在")
|
||||
|
||||
alerts = await check_clause_triggers(db, str(agreement.company_id))
|
||||
return success(data=alerts)
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Alpha 归因路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
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.company import Company
|
||||
from app.models.intervention import InterventionEvent, InterventionResult
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.alpha_attribution import attribute_alpha
|
||||
|
||||
router = APIRouter(prefix="/alpha", tags=["alpha"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[list])
|
||||
async def list_interventions(
|
||||
company_id: str | None = Query(default=None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取干预事件列表。"""
|
||||
query = (
|
||||
select(InterventionEvent)
|
||||
.join(Company, InterventionEvent.company_id == Company.id)
|
||||
.where(Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
if company_id:
|
||||
query = query.where(InterventionEvent.company_id == company_id)
|
||||
result = await db.execute(query.order_by(InterventionEvent.executed_at.desc()))
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"company_id": str(i.company_id),
|
||||
"intervention_type": i.intervention_type,
|
||||
"title": i.title,
|
||||
"description": i.description,
|
||||
"executed_at": i.executed_at.isoformat(),
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("", response_model=ApiResponse[dict])
|
||||
async def create_intervention(req: dict, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""记录干预事件。"""
|
||||
event = InterventionEvent(
|
||||
company_id=req.get("company_id"),
|
||||
intervention_type=req.get("intervention_type"),
|
||||
title=req.get("title"),
|
||||
description=req.get("description"),
|
||||
executed_by=str(user.id),
|
||||
)
|
||||
db.add(event)
|
||||
await db.flush()
|
||||
return success(data={"id": str(event.id)}, message="创建成功")
|
||||
|
||||
|
||||
@router.post("/{intervention_id}/attribute", response_model=ApiResponse[dict])
|
||||
async def attribute(intervention_id: str, req: dict, user: User = Depends(get_current_user)):
|
||||
"""AI Alpha 归因分析。"""
|
||||
result = await attribute_alpha(req.get("intervention", {}), req.get("metric_changes", {}))
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,163 @@
|
||||
"""认证路由:登录 / 注册 / 刷新 token / 获取当前用户。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_user
|
||||
from app.core.security import (
|
||||
create_access_token,
|
||||
create_refresh_token,
|
||||
decode_token,
|
||||
hash_password,
|
||||
verify_password,
|
||||
)
|
||||
from app.models.tenant import Tenant
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import (
|
||||
LoginRequest,
|
||||
RefreshRequest,
|
||||
RegisterRequest,
|
||||
TokenResponse,
|
||||
UserInfo,
|
||||
)
|
||||
from app.schemas.common import ApiResponse, success
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/register", response_model=ApiResponse[TokenResponse])
|
||||
async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""用户注册(创建新租户 + 首个用户)。"""
|
||||
# 检查邮箱是否已存在
|
||||
existing = await db.execute(select(User).where(User.email == req.email))
|
||||
if existing.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="邮箱已注册",
|
||||
)
|
||||
|
||||
# 创建租户
|
||||
tenant = Tenant(name=req.tenant_name, type="vc")
|
||||
db.add(tenant)
|
||||
await db.flush()
|
||||
|
||||
# 创建用户
|
||||
user = User(
|
||||
tenant_id=tenant.id,
|
||||
email=req.email,
|
||||
password_hash=hash_password(req.password),
|
||||
name=req.name,
|
||||
role=req.role,
|
||||
)
|
||||
db.add(user)
|
||||
await db.flush()
|
||||
|
||||
access_token = create_access_token(
|
||||
subject=user.id,
|
||||
extra_claims={"role": user.role, "tenant_id": user.tenant_id},
|
||||
)
|
||||
refresh_token = create_refresh_token(subject=user.id)
|
||||
|
||||
return success(
|
||||
data=TokenResponse(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
expires_in=settings.jwt_access_token_ttl_minutes * 60,
|
||||
),
|
||||
message="注册成功",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/login", response_model=ApiResponse[TokenResponse])
|
||||
async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""用户登录。"""
|
||||
result = await db.execute(select(User).where(User.email == req.email))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user or not verify_password(req.password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="邮箱或密码错误",
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="账户已禁用",
|
||||
)
|
||||
|
||||
access_token = create_access_token(
|
||||
subject=user.id,
|
||||
extra_claims={"role": user.role, "tenant_id": user.tenant_id},
|
||||
)
|
||||
refresh_token = create_refresh_token(subject=user.id)
|
||||
|
||||
return success(
|
||||
data=TokenResponse(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
expires_in=settings.jwt_access_token_ttl_minutes * 60,
|
||||
),
|
||||
message="登录成功",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=ApiResponse[TokenResponse])
|
||||
async def refresh_token(req: RefreshRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""刷新 access token。"""
|
||||
try:
|
||||
payload = decode_token(req.refresh_token)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的 refresh token",
|
||||
)
|
||||
|
||||
if payload.get("type") != "refresh":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的 token 类型",
|
||||
)
|
||||
|
||||
user_id = payload.get("sub")
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户不存在或已禁用",
|
||||
)
|
||||
|
||||
access_token = create_access_token(
|
||||
subject=user.id,
|
||||
extra_claims={"role": user.role, "tenant_id": user.tenant_id},
|
||||
)
|
||||
new_refresh_token = create_refresh_token(subject=user.id)
|
||||
|
||||
return success(
|
||||
data=TokenResponse(
|
||||
access_token=access_token,
|
||||
refresh_token=new_refresh_token,
|
||||
expires_in=settings.jwt_access_token_ttl_minutes * 60,
|
||||
),
|
||||
message="刷新成功",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/me", response_model=ApiResponse[UserInfo])
|
||||
async def get_me(user: User = Depends(get_current_user)):
|
||||
"""获取当前用户信息。"""
|
||||
return success(
|
||||
data=UserInfo(
|
||||
id=user.id,
|
||||
email=user.email,
|
||||
name=user.name,
|
||||
role=user.role,
|
||||
tenant_id=user.tenant_id,
|
||||
is_active=user.is_active,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,96 @@
|
||||
"""董事会路由:CRUD + 决议追踪。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
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.board import BoardMeeting
|
||||
from app.models.company import Company
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.board_agent import generate_meeting_summary, generate_questions
|
||||
|
||||
router = APIRouter(prefix="/board", tags=["board"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[list])
|
||||
async def list_board_meetings(
|
||||
company_id: str | None = Query(default=None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取董事会会议列表。"""
|
||||
query = (
|
||||
select(BoardMeeting)
|
||||
.join(Company, BoardMeeting.company_id == Company.id)
|
||||
.where(Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
if company_id:
|
||||
query = query.where(BoardMeeting.company_id == company_id)
|
||||
result = await db.execute(query.order_by(BoardMeeting.meeting_at.desc()))
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"company_id": str(i.company_id),
|
||||
"title": i.title,
|
||||
"meeting_at": i.meeting_at.isoformat() if i.meeting_at else None,
|
||||
"status": i.status,
|
||||
"agenda": i.agenda,
|
||||
"materials_summary": i.materials_summary,
|
||||
"minutes": i.minutes,
|
||||
"resolutions": i.resolutions,
|
||||
"questions": i.questions,
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("", response_model=ApiResponse[dict], status_code=status.HTTP_201_CREATED)
|
||||
async def create_board_meeting(
|
||||
req: dict,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""创建董事会会议。"""
|
||||
company_result = await db.execute(
|
||||
select(Company).where(Company.id == req.get("company_id"), Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
if not company_result.scalar_one_or_none():
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="企业不存在")
|
||||
|
||||
meeting = BoardMeeting(
|
||||
company_id=req.get("company_id"),
|
||||
title=req.get("title"),
|
||||
meeting_at=req.get("meeting_at"),
|
||||
agenda=req.get("agenda"),
|
||||
)
|
||||
db.add(meeting)
|
||||
await db.flush()
|
||||
return success(data={"id": str(meeting.id)}, message="创建成功")
|
||||
|
||||
|
||||
@router.post("/{meeting_id}/generate-summary", response_model=ApiResponse[str])
|
||||
async def generate_meeting_summary_endpoint(
|
||||
meeting_id: str,
|
||||
req: dict,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""AI 生成会前材料摘要。"""
|
||||
summary = await generate_meeting_summary(req.get("materials_text", ""))
|
||||
return success(data=summary)
|
||||
|
||||
|
||||
@router.post("/{meeting_id}/generate-questions", response_model=ApiResponse[list])
|
||||
async def generate_questions_endpoint(
|
||||
meeting_id: str,
|
||||
req: dict,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""AI 生成提问清单。"""
|
||||
questions = await generate_questions(req.get("materials_text", ""))
|
||||
return success(data=questions)
|
||||
@@ -0,0 +1,394 @@
|
||||
"""企业档案路由:CRUD + 列表分页。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import func, 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.company import Company
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.schemas.company import (
|
||||
CompanyCreate,
|
||||
CompanyDetailResponse,
|
||||
CompanyListResponse,
|
||||
CompanyResponse,
|
||||
CompanyUpdate,
|
||||
AgreementBrief,
|
||||
BoardMeetingBrief,
|
||||
HealthScoreBrief,
|
||||
HealthScoreHistoryPoint,
|
||||
MajorEventBrief,
|
||||
MilestoneBrief,
|
||||
ReportBrief,
|
||||
RiskBrief,
|
||||
SynergyBrief,
|
||||
TeamMemberBrief,
|
||||
WeakSignalBrief,
|
||||
)
|
||||
from app.models.agreement import InvestmentAgreement
|
||||
from app.models.board import BoardMeeting
|
||||
from app.models.financial_data import FinancialData
|
||||
from app.models.health_score import HealthScore
|
||||
from app.models.major_event import MajorEvent
|
||||
from app.models.milestone import MilestoneTree
|
||||
from app.models.report import MonthlyReport
|
||||
from app.models.risk import RiskEvent
|
||||
from app.models.synergy import SynergyOpportunity
|
||||
from app.models.talent import TeamMember
|
||||
from app.models.weak_signal import WeakSignal
|
||||
|
||||
router = APIRouter(prefix="/companies", tags=["companies"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[CompanyListResponse])
|
||||
async def list_companies(
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
keyword: str | None = Query(default=None, description="按名称搜索"),
|
||||
industry: str | None = Query(default=None, description="按行业筛选"),
|
||||
stage: str | None = Query(default=None, description="按融资阶段筛选"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取企业列表(分页 + 筛选)。"""
|
||||
query = select(Company).where(Company.tenant_id == user.tenant_id)
|
||||
|
||||
if keyword:
|
||||
query = query.where(Company.name.ilike(f"%{keyword}%"))
|
||||
if industry:
|
||||
query = query.where(Company.industry == industry)
|
||||
if stage:
|
||||
query = query.where(Company.stage == stage)
|
||||
|
||||
# 总数
|
||||
count_query = select(func.count()).select_from(query.subquery())
|
||||
total_result = await db.execute(count_query)
|
||||
total = total_result.scalar_one()
|
||||
|
||||
# 分页
|
||||
offset = (page - 1) * page_size
|
||||
query = query.order_by(Company.created_at.desc()).offset(offset).limit(page_size)
|
||||
result = await db.execute(query)
|
||||
companies = result.scalars().all()
|
||||
|
||||
return success(
|
||||
data=CompanyListResponse(
|
||||
items=[CompanyResponse.model_validate(c, from_attributes=True) for c in companies],
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{company_id}", response_model=ApiResponse[CompanyResponse])
|
||||
async def get_company(
|
||||
company_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取企业详情。"""
|
||||
result = await db.execute(
|
||||
select(Company).where(Company.id == company_id, Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
company = result.scalar_one_or_none()
|
||||
if not company:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="企业不存在")
|
||||
return success(data=CompanyResponse.model_validate(company, from_attributes=True))
|
||||
|
||||
|
||||
@router.get("/{company_id}/detail", response_model=ApiResponse[CompanyDetailResponse])
|
||||
async def get_company_detail(
|
||||
company_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取企业详情聚合数据 — 工作台使用。
|
||||
|
||||
聚合:企业基本信息 + 最新健康度 + 最近月报 + 未解决风险 + 弱信号 + 活跃协议 + 董事会会议 + 财务数据。
|
||||
"""
|
||||
# 企业基本信息
|
||||
result = await db.execute(
|
||||
select(Company).where(Company.id == company_id, Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
company = result.scalar_one_or_none()
|
||||
if not company:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="企业不存在")
|
||||
|
||||
# 最新健康度评分
|
||||
health_result = await db.execute(
|
||||
select(HealthScore)
|
||||
.where(HealthScore.company_id == company_id)
|
||||
.order_by(HealthScore.calculated_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
health = health_result.scalar_one_or_none()
|
||||
health_brief = HealthScoreBrief(
|
||||
total_score=health.total_score,
|
||||
financial_score=health.financial_score,
|
||||
operational_score=health.operational_score,
|
||||
ai_commercial_score=health.ai_commercial_score,
|
||||
ai_cost_score=health.ai_cost_score,
|
||||
org_talent_score=getattr(health, "org_talent_score", None),
|
||||
product_tech_score=getattr(health, "product_tech_score", None),
|
||||
market_compete_score=getattr(health, "market_compete_score", None),
|
||||
governance_score=getattr(health, "governance_score", None),
|
||||
financing_score=getattr(health, "financing_score", None),
|
||||
synergy_score=getattr(health, "synergy_score", None),
|
||||
ai_model_product_score=getattr(health, "ai_model_product_score", None),
|
||||
data_compliance_score=getattr(health, "data_compliance_score", None),
|
||||
team_tech_score=getattr(health, "team_tech_score", None),
|
||||
customer_success_score=getattr(health, "customer_success_score", None),
|
||||
trend=health.trend,
|
||||
calculated_at=health.calculated_at,
|
||||
) if health else None
|
||||
|
||||
# 最近 5 条月报
|
||||
reports_result = await db.execute(
|
||||
select(MonthlyReport)
|
||||
.where(MonthlyReport.company_id == company_id)
|
||||
.order_by(MonthlyReport.period_year.desc(), MonthlyReport.period_month.desc())
|
||||
.limit(5)
|
||||
)
|
||||
reports = reports_result.scalars().all()
|
||||
report_briefs = [
|
||||
ReportBrief(
|
||||
id=r.id, period_year=r.period_year, period_month=r.period_month,
|
||||
status=r.status, ai_summary=r.ai_summary, submitted_at=r.submitted_at,
|
||||
) for r in reports
|
||||
]
|
||||
|
||||
# 未解决风险
|
||||
risks_result = await db.execute(
|
||||
select(RiskEvent)
|
||||
.where(RiskEvent.company_id == company_id, RiskEvent.status.in_(["open", "assigned", "in_progress"]))
|
||||
.order_by(RiskEvent.identified_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
risks = risks_result.scalars().all()
|
||||
risk_briefs = [
|
||||
RiskBrief(
|
||||
id=r.id, type=r.type, severity=r.severity, status=r.status,
|
||||
title=r.title, identified_at=r.identified_at,
|
||||
) for r in risks
|
||||
]
|
||||
|
||||
# 最近弱信号
|
||||
signals_result = await db.execute(
|
||||
select(WeakSignal)
|
||||
.where(WeakSignal.company_id == company_id)
|
||||
.order_by(WeakSignal.detected_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
signals = signals_result.scalars().all()
|
||||
signal_briefs = [
|
||||
WeakSignalBrief(
|
||||
id=s.id, signal_type=s.signal_type, content=s.content,
|
||||
confidence=s.confidence, risk_probability=s.risk_probability,
|
||||
status=s.status, detected_at=s.detected_at,
|
||||
) for s in signals
|
||||
]
|
||||
|
||||
# 活跃协议
|
||||
agreements_result = await db.execute(
|
||||
select(InvestmentAgreement)
|
||||
.where(InvestmentAgreement.company_id == company_id, InvestmentAgreement.status == "active")
|
||||
.order_by(InvestmentAgreement.created_at.desc())
|
||||
)
|
||||
agreements = agreements_result.scalars().all()
|
||||
agreement_briefs = [
|
||||
AgreementBrief(id=a.id, title=a.title, status=a.status, signed_at=a.signed_at)
|
||||
for a in agreements
|
||||
]
|
||||
|
||||
# 最近董事会会议
|
||||
board_result = await db.execute(
|
||||
select(BoardMeeting)
|
||||
.where(BoardMeeting.company_id == company_id)
|
||||
.order_by(BoardMeeting.created_at.desc())
|
||||
.limit(5)
|
||||
)
|
||||
meetings = board_result.scalars().all()
|
||||
meeting_briefs = [
|
||||
BoardMeetingBrief(id=m.id, title=m.title, status=m.status, meeting_at=m.meeting_at)
|
||||
for m in meetings
|
||||
]
|
||||
|
||||
# 财务数据统计
|
||||
fin_count_result = await db.execute(
|
||||
select(func.count()).select_from(
|
||||
select(FinancialData).where(FinancialData.company_id == company_id).subquery()
|
||||
)
|
||||
)
|
||||
fin_count = fin_count_result.scalar_one()
|
||||
|
||||
latest_fin_result = await db.execute(
|
||||
select(FinancialData)
|
||||
.where(FinancialData.company_id == company_id)
|
||||
.order_by(FinancialData.period_year.desc(), FinancialData.period_month.desc())
|
||||
.limit(1)
|
||||
)
|
||||
latest_fin = latest_fin_result.scalar_one_or_none()
|
||||
latest_financial = latest_fin.data_json if latest_fin else None
|
||||
|
||||
# 健康度历史趋势(最近 12 条)
|
||||
history_result = await db.execute(
|
||||
select(HealthScore)
|
||||
.where(HealthScore.company_id == company_id)
|
||||
.order_by(HealthScore.calculated_at.asc())
|
||||
.limit(12)
|
||||
)
|
||||
history_scores = history_result.scalars().all()
|
||||
health_score_history = [
|
||||
HealthScoreHistoryPoint(
|
||||
period=s.calculated_at.strftime("%Y-%m"),
|
||||
total_score=s.total_score,
|
||||
calculated_at=s.calculated_at,
|
||||
)
|
||||
for s in history_scores
|
||||
]
|
||||
|
||||
# 重大事项(最近 10 条)
|
||||
events_result = await db.execute(
|
||||
select(MajorEvent)
|
||||
.where(MajorEvent.company_id == company_id)
|
||||
.order_by(MajorEvent.created_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
events = events_result.scalars().all()
|
||||
major_events = [
|
||||
MajorEventBrief(
|
||||
id=e.id, event_type=e.event_type, title=e.title,
|
||||
description=e.description, severity=e.severity,
|
||||
status=e.status, occurred_at=e.occurred_at,
|
||||
) for e in events
|
||||
]
|
||||
|
||||
# 里程碑
|
||||
milestone_result = await db.execute(
|
||||
select(MilestoneTree)
|
||||
.where(MilestoneTree.company_id == company_id)
|
||||
.order_by(MilestoneTree.target_date.desc().nulls_last())
|
||||
.limit(20)
|
||||
)
|
||||
milestones = milestone_result.scalars().all()
|
||||
milestone_briefs = [
|
||||
MilestoneBrief(
|
||||
id=m.id, name=m.name, status=m.status, is_current=m.is_current,
|
||||
target_date=m.target_date, actual_date=m.actual_date,
|
||||
description=m.description,
|
||||
) for m in milestones
|
||||
]
|
||||
|
||||
# 团队成员
|
||||
team_result = await db.execute(
|
||||
select(TeamMember)
|
||||
.where(TeamMember.company_id == company_id)
|
||||
.order_by(TeamMember.is_key_person.desc(), TeamMember.joined_at.desc())
|
||||
.limit(20)
|
||||
)
|
||||
team_members_data = team_result.scalars().all()
|
||||
team_members = [
|
||||
TeamMemberBrief(
|
||||
id=t.id, name=t.name, role=t.role, is_key_person=t.is_key_person,
|
||||
stability_score=t.stability_score, joined_at=t.joined_at,
|
||||
) for t in team_members_data
|
||||
]
|
||||
|
||||
# 协同机会(涉及该企业的)
|
||||
synergy_result = await db.execute(
|
||||
select(SynergyOpportunity)
|
||||
.where(
|
||||
(SynergyOpportunity.company_a_id == company_id) |
|
||||
(SynergyOpportunity.company_b_id == company_id)
|
||||
)
|
||||
.order_by(SynergyOpportunity.created_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
synergies = synergy_result.scalars().all()
|
||||
synergy_briefs = [
|
||||
SynergyBrief(
|
||||
id=s.id, type=s.type, title=s.title,
|
||||
description=s.description, status=s.status,
|
||||
match_reason=s.match_reason,
|
||||
) for s in synergies
|
||||
]
|
||||
|
||||
return success(data=CompanyDetailResponse(
|
||||
company=CompanyResponse.model_validate(company, from_attributes=True),
|
||||
health_score=health_brief,
|
||||
health_score_history=health_score_history,
|
||||
recent_reports=report_briefs,
|
||||
open_risks=risk_briefs,
|
||||
recent_weak_signals=signal_briefs,
|
||||
active_agreements=agreement_briefs,
|
||||
recent_board_meetings=meeting_briefs,
|
||||
major_events=major_events,
|
||||
milestones=milestone_briefs,
|
||||
team_members=team_members,
|
||||
synergy_opportunities=synergy_briefs,
|
||||
financial_data_count=fin_count,
|
||||
latest_financial=latest_financial,
|
||||
))
|
||||
|
||||
|
||||
@router.post("", response_model=ApiResponse[CompanyResponse], status_code=status.HTTP_201_CREATED)
|
||||
async def create_company(
|
||||
req: CompanyCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""创建企业。"""
|
||||
company = Company(tenant_id=user.tenant_id, **req.model_dump())
|
||||
db.add(company)
|
||||
await db.flush()
|
||||
return success(
|
||||
data=CompanyResponse.model_validate(company, from_attributes=True),
|
||||
message="创建成功",
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{company_id}", response_model=ApiResponse[CompanyResponse])
|
||||
async def update_company(
|
||||
company_id: str,
|
||||
req: CompanyUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""更新企业信息。"""
|
||||
result = await db.execute(
|
||||
select(Company).where(Company.id == company_id, Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
company = result.scalar_one_or_none()
|
||||
if not company:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="企业不存在")
|
||||
|
||||
update_data = req.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(company, key, value)
|
||||
|
||||
await db.flush()
|
||||
return success(
|
||||
data=CompanyResponse.model_validate(company, from_attributes=True),
|
||||
message="更新成功",
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{company_id}", response_model=ApiResponse[None])
|
||||
async def delete_company(
|
||||
company_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""删除企业。"""
|
||||
result = await db.execute(
|
||||
select(Company).where(Company.id == company_id, Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
company = result.scalar_one_or_none()
|
||||
if not company:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="企业不存在")
|
||||
|
||||
await db.delete(company)
|
||||
return success(message="删除成功")
|
||||
@@ -0,0 +1,101 @@
|
||||
"""AI Copilot 路由 — SSE 流式对话。
|
||||
|
||||
为投资人和创始人提供 AI 副驾驶对话能力。
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
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
|
||||
from app.services.llm_client import llm_client
|
||||
|
||||
router = APIRouter(prefix="/copilot", tags=["copilot"])
|
||||
|
||||
|
||||
class CopilotMessage(BaseModel):
|
||||
"""Copilot 对话请求。"""
|
||||
|
||||
message: str = Field(..., min_length=1, max_length=4000)
|
||||
context: dict | None = Field(default=None, description="可选上下文(企业ID/月报ID等)")
|
||||
|
||||
|
||||
SYSTEM_PROMPT_INVESTOR = """你是 AIPortPilot 投后管理系统的 AI 副驾驶,服务于投资人用户。
|
||||
|
||||
你的职责:
|
||||
1. 分析被投企业的经营状况和财务健康度
|
||||
2. 识别潜在风险并提供预警建议
|
||||
3. 协助撰写投后管理报告和建议
|
||||
4. 解答关于企业治理、融资策略的问题
|
||||
|
||||
回答要求:
|
||||
- 专业、简洁、有数据支撑
|
||||
- 如需引用数据,明确标注来源
|
||||
- 对于不确定的信息,坦诚说明
|
||||
- 使用中文回答"""
|
||||
|
||||
|
||||
SYSTEM_PROMPT_FOUNDER = """你是 AIPortPilot 投后管理系统的 AI 副驾驶,服务于创始人用户。
|
||||
|
||||
你的职责:
|
||||
1. 协助撰写和优化月报内容
|
||||
2. 分析企业经营数据,提供改进建议
|
||||
3. 解答融资、团队管理、业务增长等问题
|
||||
4. 提供行业趋势和竞品分析参考
|
||||
|
||||
回答要求:
|
||||
- 实用、可操作、接地气
|
||||
- 关注创始人的实际痛点
|
||||
- 使用中文回答"""
|
||||
|
||||
|
||||
@router.post("/chat")
|
||||
async def copilot_chat(
|
||||
req: CopilotMessage,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""AI Copilot 对话(SSE 流式输出)。
|
||||
|
||||
根据用户角色使用不同的 system prompt。
|
||||
"""
|
||||
system_prompt = (
|
||||
SYSTEM_PROMPT_FOUNDER if user.role == "founder" else SYSTEM_PROMPT_INVESTOR
|
||||
)
|
||||
|
||||
# 构建上下文
|
||||
context_str = ""
|
||||
if req.context:
|
||||
context_parts = []
|
||||
for key, value in req.context.items():
|
||||
context_parts.append(f"{key}: {value}")
|
||||
context_str = f"\n\n当前上下文:\n" + "\n".join(context_parts)
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": system_prompt + context_str},
|
||||
{"role": "user", "content": f"<<<USER_INPUT>>>\n{req.message}\n<<<END_USER_INPUT>>>"},
|
||||
]
|
||||
|
||||
async def event_stream():
|
||||
"""SSE 流式输出。"""
|
||||
try:
|
||||
async for token in llm_client.chat_stream(messages, temperature=0.7, max_tokens=2000):
|
||||
yield f"data: {json.dumps({'type': 'token', 'content': token})}\n\n"
|
||||
yield f"data: {json.dumps({'type': 'done'})}\n\n"
|
||||
except Exception as e:
|
||||
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
event_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
"""客户增长路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
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.customer_plan import CustomerAcquisitionPlan
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.customer_growth_agent import generate_customer_plan
|
||||
|
||||
router = APIRouter(prefix="/customer-plans", tags=["customer-plans"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[list])
|
||||
async def list_plans(company_id: str | None = Query(default=None), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""获取客户获取方案列表。"""
|
||||
query = select(CustomerAcquisitionPlan)
|
||||
if company_id:
|
||||
query = query.where(CustomerAcquisitionPlan.company_id == company_id)
|
||||
result = await db.execute(query.order_by(CustomerAcquisitionPlan.created_at.desc()))
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"company_id": str(i.company_id),
|
||||
"target_customer": i.target_customer,
|
||||
"entry_angle": i.entry_angle,
|
||||
"pricing_strategy": i.pricing_strategy,
|
||||
"execution_status": i.execution_status,
|
||||
"result": i.result,
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("/generate", response_model=ApiResponse[dict])
|
||||
async def generate_plan(req: dict, user: User = Depends(get_current_user)):
|
||||
"""AI 生成客户获取方案。"""
|
||||
result = await generate_customer_plan(req.get("company_context", ""), req.get("lp_resources", ""))
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,35 @@
|
||||
"""客户成功运营路由 — QBR + Expansion + Churn Risk。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
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
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.qbr_generator import generate_qbr
|
||||
from app.services.expansion_play import identify_expansion_opportunities
|
||||
from app.services.churn_risk_detector import detect_churn_risk
|
||||
|
||||
router = APIRouter(prefix="/customer-success", tags=["customer-success"])
|
||||
|
||||
|
||||
@router.post("/qbr", response_model=ApiResponse[dict])
|
||||
async def generate_qbr_report(req: dict, user: User = Depends(get_current_user)):
|
||||
"""自动生成 QBR 季度业务回顾。"""
|
||||
result = await generate_qbr(req.get("company_id", ""), req.get("quarter_data", ""))
|
||||
return success(data=result)
|
||||
|
||||
|
||||
@router.post("/expansion", response_model=ApiResponse[list])
|
||||
async def identify_expansion(req: dict, user: User = Depends(get_current_user)):
|
||||
"""识别扩展机会。"""
|
||||
result = await identify_expansion_opportunities(req.get("company_data", ""))
|
||||
return success(data=result)
|
||||
|
||||
|
||||
@router.get("/churn-risk", response_model=ApiResponse[list])
|
||||
async def get_churn_risk(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""获取流失风险预警。"""
|
||||
result = await detect_churn_risk(db, user.tenant_id)
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,300 @@
|
||||
"""健康度评分 + 仪表盘路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import func, 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.company import Company
|
||||
from app.models.health_score import HealthScore
|
||||
from app.models.report import MonthlyReport
|
||||
from app.models.risk import RiskEvent
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.schemas.health_score import DashboardSummary, HealthScoreResponse
|
||||
from app.services.predictor import predict_trend, detect_anomalies
|
||||
|
||||
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
||||
|
||||
# 14 维度 key 列表
|
||||
_DIMENSION_KEYS = [
|
||||
"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",
|
||||
]
|
||||
|
||||
|
||||
@router.get("/summary", response_model=ApiResponse[DashboardSummary])
|
||||
async def get_dashboard_summary(
|
||||
company_id: str | None = Query(default=None, description="指定企业 ID,不传则汇总全租户"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取仪表盘汇总数据。支持按企业过滤。"""
|
||||
tenant_id = user.tenant_id
|
||||
|
||||
# 企业总数(单企业视角时为 1)
|
||||
if company_id:
|
||||
total_companies = 1
|
||||
else:
|
||||
companies_result = await db.execute(
|
||||
select(func.count()).select_from(Company).where(Company.tenant_id == tenant_id)
|
||||
)
|
||||
total_companies = companies_result.scalar_one()
|
||||
|
||||
# 平均健康度(按企业过滤时只算该企业)
|
||||
avg_query = (
|
||||
select(func.avg(HealthScore.total_score))
|
||||
.join(Company, HealthScore.company_id == Company.id)
|
||||
.where(Company.tenant_id == tenant_id)
|
||||
)
|
||||
if company_id:
|
||||
avg_query = avg_query.where(HealthScore.company_id == company_id)
|
||||
avg_result = await db.execute(avg_query)
|
||||
avg_score = avg_result.scalar_one()
|
||||
avg_health_score = float(avg_score) if avg_score else 0.0
|
||||
|
||||
# 高风险事件数
|
||||
risk_query = (
|
||||
select(func.count())
|
||||
.select_from(RiskEvent)
|
||||
.join(Company, RiskEvent.company_id == Company.id)
|
||||
.where(Company.tenant_id == tenant_id, RiskEvent.status.in_(["open", "assigned", "in_progress"]))
|
||||
)
|
||||
if company_id:
|
||||
risk_query = risk_query.where(RiskEvent.company_id == company_id)
|
||||
risk_result = await db.execute(risk_query)
|
||||
high_risk_count = risk_result.scalar_one()
|
||||
|
||||
# 待审阅月报数
|
||||
pending_query = (
|
||||
select(func.count())
|
||||
.select_from(MonthlyReport)
|
||||
.join(Company, MonthlyReport.company_id == Company.id)
|
||||
.where(Company.tenant_id == tenant_id, MonthlyReport.status.in_(["submitted", "ai_parsed"]))
|
||||
)
|
||||
if company_id:
|
||||
pending_query = pending_query.where(MonthlyReport.company_id == company_id)
|
||||
pending_result = await db.execute(pending_query)
|
||||
pending_reports = pending_result.scalar_one()
|
||||
|
||||
# 最近评分 — 每家企业只取最新一条(避免历史数据导致企业重复)
|
||||
subq = (
|
||||
select(
|
||||
HealthScore.company_id,
|
||||
func.max(HealthScore.calculated_at).label("max_at"),
|
||||
)
|
||||
.join(Company, HealthScore.company_id == Company.id)
|
||||
.where(Company.tenant_id == tenant_id)
|
||||
.group_by(HealthScore.company_id)
|
||||
)
|
||||
if company_id:
|
||||
subq = subq.where(HealthScore.company_id == company_id)
|
||||
subq = subq.subquery()
|
||||
|
||||
recent_query = (
|
||||
select(HealthScore, Company.name.label("company_name"))
|
||||
.join(Company, HealthScore.company_id == Company.id)
|
||||
.join(subq, (HealthScore.company_id == subq.c.company_id) & (HealthScore.calculated_at == subq.c.max_at))
|
||||
.order_by(HealthScore.calculated_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
recent_result = await db.execute(recent_query)
|
||||
recent_rows = recent_result.all()
|
||||
recent_scores = []
|
||||
for row in recent_rows:
|
||||
score = row[0]
|
||||
company_name = row[1]
|
||||
resp = HealthScoreResponse.model_validate(score, from_attributes=True)
|
||||
resp.company_name = company_name
|
||||
recent_scores.append(resp)
|
||||
|
||||
return success(
|
||||
data=DashboardSummary(
|
||||
total_companies=total_companies,
|
||||
avg_health_score=round(avg_health_score, 1),
|
||||
high_risk_count=high_risk_count,
|
||||
pending_reports=pending_reports,
|
||||
recent_scores=recent_scores,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/scores", response_model=ApiResponse[list[HealthScoreResponse]])
|
||||
async def list_health_scores(
|
||||
company_id: str | None = Query(default=None),
|
||||
limit: int = Query(default=20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取健康度评分列表 — 每家企业只返回最新一条。"""
|
||||
subq = (
|
||||
select(
|
||||
HealthScore.company_id,
|
||||
func.max(HealthScore.calculated_at).label("max_at"),
|
||||
)
|
||||
.join(Company, HealthScore.company_id == Company.id)
|
||||
.where(Company.tenant_id == user.tenant_id)
|
||||
.group_by(HealthScore.company_id)
|
||||
)
|
||||
if company_id:
|
||||
subq = subq.where(HealthScore.company_id == company_id)
|
||||
subq = subq.subquery()
|
||||
|
||||
query = (
|
||||
select(HealthScore, Company.name.label("company_name"))
|
||||
.join(Company, HealthScore.company_id == Company.id)
|
||||
.join(subq, (HealthScore.company_id == subq.c.company_id) & (HealthScore.calculated_at == subq.c.max_at))
|
||||
.order_by(HealthScore.calculated_at.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
result = await db.execute(query)
|
||||
scores = []
|
||||
for row in result.all():
|
||||
score = row[0]
|
||||
company_name = row[1]
|
||||
resp = HealthScoreResponse.model_validate(score, from_attributes=True)
|
||||
resp.company_name = company_name
|
||||
scores.append(resp)
|
||||
return success(data=scores)
|
||||
|
||||
|
||||
@router.get("/heatmap", response_model=ApiResponse[list[dict]])
|
||||
async def get_health_heatmap(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取健康度热力图数据 — 企业 × 维度评分矩阵。
|
||||
|
||||
返回格式:[{ company_id, company_name, scores: { dimension: score } }]
|
||||
"""
|
||||
# 获取租户下所有企业
|
||||
companies_result = await db.execute(
|
||||
select(Company).where(Company.tenant_id == user.tenant_id).order_by(Company.name)
|
||||
)
|
||||
companies = companies_result.scalars().all()
|
||||
|
||||
# 获取每个企业最新评分
|
||||
heatmap = []
|
||||
for company in companies:
|
||||
score_result = await db.execute(
|
||||
select(HealthScore)
|
||||
.where(HealthScore.company_id == company.id)
|
||||
.order_by(HealthScore.calculated_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
score = score_result.scalar_one_or_none()
|
||||
scores_dict = {}
|
||||
if score:
|
||||
for dim_key in _DIMENSION_KEYS:
|
||||
val = getattr(score, dim_key, None)
|
||||
if val is not None:
|
||||
scores_dict[dim_key] = val
|
||||
heatmap.append({
|
||||
"company_id": company.id,
|
||||
"company_name": company.name,
|
||||
"total_score": score.total_score if score else None,
|
||||
"scores": scores_dict,
|
||||
})
|
||||
|
||||
return success(data=heatmap)
|
||||
|
||||
|
||||
@router.get("/trends", response_model=ApiResponse[list[dict]])
|
||||
async def get_health_trends(
|
||||
company_id: str | None = Query(default=None, description="指定企业 ID,不传则返回全部企业"),
|
||||
months: int = Query(default=6, ge=1, le=24, description="趋势月数"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取健康度趋势数据 — 按企业分组,每家企业一条折线。
|
||||
|
||||
返回格式:[{ company_id, company_name, data: [{ period, score }] }]
|
||||
"""
|
||||
query = (
|
||||
select(HealthScore, Company.name.label("company_name"))
|
||||
.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)
|
||||
|
||||
query = query.order_by(HealthScore.calculated_at.asc()).limit(months * 100)
|
||||
result = await db.execute(query)
|
||||
|
||||
# 按企业分组
|
||||
by_company: dict[str, dict[str, list]] = {}
|
||||
for row in result.all():
|
||||
score = row[0]
|
||||
cname = row[1]
|
||||
cid = str(score.company_id)
|
||||
if cid not in by_company:
|
||||
by_company[cid] = {"company_name": cname, "scores": []}
|
||||
by_company[cid]["scores"].append(score)
|
||||
|
||||
trends = []
|
||||
for cid, info in by_company.items():
|
||||
# 每月取最新一条
|
||||
monthly: dict[str, float] = {}
|
||||
for s in info["scores"]:
|
||||
period = s.calculated_at.strftime("%Y-%m")
|
||||
monthly[period] = s.total_score
|
||||
|
||||
data_points = [
|
||||
{"period": p, "score": round(v, 1)}
|
||||
for p, v in sorted(monthly.items())
|
||||
]
|
||||
trends.append({
|
||||
"company_id": cid,
|
||||
"company_name": info["company_name"],
|
||||
"data": data_points,
|
||||
})
|
||||
|
||||
return success(data=trends)
|
||||
|
||||
|
||||
@router.get("/forecasts", response_model=ApiResponse[dict])
|
||||
async def get_health_forecasts(
|
||||
company_id: str | None = Query(default=None, description="指定企业 ID,不传则汇总全租户"),
|
||||
months_ahead: int = Query(default=3, ge=1, le=6, description="预测月数"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取健康度预测数据 — 基于历史评分预测未来趋势 + 异常检测。
|
||||
|
||||
返回格式:{ predictions: [...], trend_direction, confidence, anomalies: [...] }
|
||||
"""
|
||||
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)
|
||||
|
||||
query = query.order_by(HealthScore.calculated_at.asc()).limit(24)
|
||||
result = await db.execute(query)
|
||||
scores = result.scalars().all()
|
||||
|
||||
historical = [s.total_score for s in scores]
|
||||
forecast = predict_trend(historical, months_ahead)
|
||||
|
||||
# 异常检测 — 各维度
|
||||
anomalies_by_dim: dict[str, list[int]] = {}
|
||||
for dim_key in _DIMENSION_KEYS:
|
||||
dim_values = [getattr(s, dim_key) for s in scores if getattr(s, dim_key) is not None]
|
||||
if len(dim_values) >= 3:
|
||||
dim_anomalies = detect_anomalies(dim_values)
|
||||
if dim_anomalies:
|
||||
anomalies_by_dim[dim_key] = dim_anomalies
|
||||
|
||||
return success(data={
|
||||
"predictions": forecast.get("predicted", []),
|
||||
"slope": forecast.get("slope", 0),
|
||||
"confidence": forecast.get("confidence", 0),
|
||||
"trend_direction": "up" if forecast.get("slope", 0) > 1 else "down" if forecast.get("slope", 0) < -1 else "stable",
|
||||
"anomalies": anomalies_by_dim,
|
||||
"historical_count": len(historical),
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
"""数据源管理路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
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.data_source import DataSource
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
|
||||
router = APIRouter(prefix="/data-sources", tags=["data-sources"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[list])
|
||||
async def list_data_sources(
|
||||
company_id: str | None = Query(default=None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取数据源列表。"""
|
||||
query = select(DataSource).where(DataSource.tenant_id == user.tenant_id)
|
||||
if company_id:
|
||||
query = query.where(DataSource.company_id == company_id)
|
||||
result = await db.execute(query)
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"company_id": str(i.company_id) if i.company_id else None,
|
||||
"source_type": i.source_type,
|
||||
"name": i.name,
|
||||
"status": i.status,
|
||||
"last_synced_at": i.last_synced_at.isoformat() if i.last_synced_at else None,
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("", response_model=ApiResponse[dict])
|
||||
async def create_data_source(req: dict, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""配置数据源。"""
|
||||
ds = DataSource(
|
||||
tenant_id=user.tenant_id,
|
||||
company_id=req.get("company_id"),
|
||||
source_type=req.get("source_type"),
|
||||
name=req.get("name"),
|
||||
api_endpoint=req.get("api_endpoint"),
|
||||
config=req.get("config"),
|
||||
)
|
||||
db.add(ds)
|
||||
await db.flush()
|
||||
return success(data={"id": str(ds.id)}, message="创建成功")
|
||||
@@ -0,0 +1,68 @@
|
||||
"""决策前哨路由:CRUD + 场景分析查询。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
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.company import Company
|
||||
from app.models.decision_sentinel import DecisionSentinel
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.decision_sentinel_agent import analyze_scenarios, identify_decision_points
|
||||
|
||||
router = APIRouter(prefix="/decision-sentinels", tags=["decision-sentinels"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[list])
|
||||
async def list_sentinels(
|
||||
company_id: str | None = Query(default=None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取决策前哨列表。"""
|
||||
query = (
|
||||
select(DecisionSentinel)
|
||||
.join(Company, DecisionSentinel.company_id == Company.id)
|
||||
.where(Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
if company_id:
|
||||
query = query.where(DecisionSentinel.company_id == company_id)
|
||||
result = await db.execute(query.order_by(DecisionSentinel.identified_at.desc()))
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"company_id": str(i.company_id),
|
||||
"decision_type": i.decision_type,
|
||||
"title": i.title,
|
||||
"description": i.description,
|
||||
"signals": i.signals,
|
||||
"scenarios": i.scenarios,
|
||||
"status": i.status,
|
||||
"identified_at": i.identified_at.isoformat(),
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("/identify", response_model=ApiResponse[list])
|
||||
async def identify_sentinels(
|
||||
req: dict,
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""AI 识别决策岔路口。"""
|
||||
points = await identify_decision_points(req.get("company_context", ""))
|
||||
return success(data=points)
|
||||
|
||||
|
||||
@router.post("/{sentinel_id}/analyze", response_model=ApiResponse[dict])
|
||||
async def analyze_sentinel_scenarios(
|
||||
sentinel_id: str,
|
||||
req: dict,
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""AI 生成场景分析。"""
|
||||
scenarios = await analyze_scenarios(req.get("decision", {}))
|
||||
return success(data=scenarios)
|
||||
@@ -0,0 +1,47 @@
|
||||
"""数字孪生路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
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.digital_twin import DigitalTwinModel
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.digital_twin_engine import build_twin_model, simulate_scenario
|
||||
|
||||
router = APIRouter(prefix="/digital-twins", tags=["digital-twins"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[list])
|
||||
async def list_twins(company_id: str = Query(...), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""获取数字孪生模型列表。"""
|
||||
result = await db.execute(
|
||||
select(DigitalTwinModel).where(DigitalTwinModel.company_id == company_id)
|
||||
)
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"company_id": str(i.company_id),
|
||||
"model_params": i.model_params,
|
||||
"scenarios": i.scenarios,
|
||||
"accuracy_score": i.accuracy_score,
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("/build", response_model=ApiResponse[dict])
|
||||
async def build_twin(req: dict, user: User = Depends(get_current_user)):
|
||||
"""构建数字孪生模型。"""
|
||||
result = await build_twin_model(req.get("company_data", ""))
|
||||
return success(data=result)
|
||||
|
||||
|
||||
@router.post("/simulate", response_model=ApiResponse[dict])
|
||||
async def simulate(req: dict, user: User = Depends(get_current_user)):
|
||||
"""模拟决策场景。"""
|
||||
result = await simulate_scenario(req.get("model_params", {}), req.get("scenario", ""))
|
||||
return success(data=result)
|
||||
@@ -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,106 @@
|
||||
"""重大事项 + 追问清单路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
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.company import Company
|
||||
from app.models.inquiry import InquiryList
|
||||
from app.models.major_event import MajorEvent
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.event_detector import detect_major_events
|
||||
from app.services.inquiry_generator import generate_inquiry_questions
|
||||
|
||||
router = APIRouter(prefix="/events", tags=["events"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[list])
|
||||
async def list_events(
|
||||
company_id: str | None = Query(default=None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取重大事项列表。"""
|
||||
query = (
|
||||
select(MajorEvent)
|
||||
.join(Company, MajorEvent.company_id == Company.id)
|
||||
.where(Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
if company_id:
|
||||
query = query.where(MajorEvent.company_id == company_id)
|
||||
result = await db.execute(query.order_by(MajorEvent.created_at.desc()))
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"company_id": str(i.company_id),
|
||||
"event_type": i.event_type,
|
||||
"title": i.title,
|
||||
"description": i.description,
|
||||
"severity": i.severity,
|
||||
"source": i.source,
|
||||
"evidence": i.evidence,
|
||||
"status": i.status,
|
||||
"occurred_at": i.occurred_at.isoformat() if i.occurred_at else None,
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("/detect", response_model=ApiResponse[list])
|
||||
async def detect_events(
|
||||
req: dict,
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""AI 从月报中识别重大事项。"""
|
||||
events = await detect_major_events(req.get("report_content", ""))
|
||||
return success(data=events)
|
||||
|
||||
|
||||
router_inquiries = APIRouter(prefix="/inquiries", tags=["inquiries"])
|
||||
|
||||
|
||||
@router_inquiries.get("", response_model=ApiResponse[list])
|
||||
async def list_inquiries(
|
||||
company_id: str | None = Query(default=None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取追问清单列表。"""
|
||||
query = (
|
||||
select(InquiryList)
|
||||
.join(Company, InquiryList.company_id == Company.id)
|
||||
.where(Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
if company_id:
|
||||
query = query.where(InquiryList.company_id == company_id)
|
||||
result = await db.execute(query.order_by(InquiryList.sent_at.desc()))
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"company_id": str(i.company_id),
|
||||
"report_id": i.report_id,
|
||||
"questions": i.questions,
|
||||
"status": i.status,
|
||||
"sent_at": i.sent_at.isoformat(),
|
||||
"answered_at": i.answered_at.isoformat() if i.answered_at else None,
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router_inquiries.post("/generate", response_model=ApiResponse[list])
|
||||
async def generate_inquiries(
|
||||
req: dict,
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""AI 生成追问清单。"""
|
||||
questions = await generate_inquiry_questions(
|
||||
req.get("report_content", ""),
|
||||
req.get("structured_data"),
|
||||
)
|
||||
return success(data=questions)
|
||||
@@ -0,0 +1,54 @@
|
||||
"""退出预测路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
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.company import Company
|
||||
from app.models.exit_prediction import ExitPrediction
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.exit_predictor import predict_exit
|
||||
|
||||
router = APIRouter(prefix="/exit-predictions", tags=["exit-predictions"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[list])
|
||||
async def list_exit_predictions(
|
||||
company_id: str | None = Query(default=None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取退出预测列表。"""
|
||||
query = (
|
||||
select(ExitPrediction)
|
||||
.join(Company, ExitPrediction.company_id == Company.id)
|
||||
.where(Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
if company_id:
|
||||
query = query.where(ExitPrediction.company_id == company_id)
|
||||
result = await db.execute(query.order_by(ExitPrediction.created_at.desc()))
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"company_id": str(i.company_id),
|
||||
"exit_path": i.exit_path,
|
||||
"timing_window": i.timing_window,
|
||||
"expected_return": i.expected_return,
|
||||
"hold_return": i.hold_return,
|
||||
"confidence": i.confidence,
|
||||
"signals": i.signals,
|
||||
"recommendation": i.recommendation,
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("/predict", response_model=ApiResponse[dict])
|
||||
async def predict(req: dict, user: User = Depends(get_current_user)):
|
||||
"""AI 退出时机预测。"""
|
||||
result = await predict_exit(req.get("company_data", ""))
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,89 @@
|
||||
"""财务数据路由:CRUD + 校验。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
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.company import Company
|
||||
from app.models.financial_data import FinancialData
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.financial_validator import validate_financial_data
|
||||
|
||||
router = APIRouter(prefix="/financial", tags=["financial"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[list])
|
||||
async def list_financial_data(
|
||||
company_id: str = Query(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取企业财务数据列表。"""
|
||||
result = await db.execute(
|
||||
select(FinancialData)
|
||||
.join(Company, FinancialData.company_id == Company.id)
|
||||
.where(Company.tenant_id == user.tenant_id, FinancialData.company_id == company_id)
|
||||
.order_by(FinancialData.period_year.desc(), FinancialData.period_month.desc())
|
||||
)
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"company_id": str(i.company_id),
|
||||
"period_year": i.period_year,
|
||||
"period_month": i.period_month,
|
||||
"statement_type": i.statement_type,
|
||||
"data_json": i.data_json,
|
||||
"credibility_score": i.credibility_score,
|
||||
"validation_result": i.validation_result,
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("", response_model=ApiResponse[dict], status_code=status.HTTP_201_CREATED)
|
||||
async def create_financial_data(
|
||||
req: dict,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""创建财务数据并自动校验。"""
|
||||
company_id = req.get("company_id")
|
||||
company_result = await db.execute(
|
||||
select(Company).where(Company.id == company_id, Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
if not company_result.scalar_one_or_none():
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="企业不存在")
|
||||
|
||||
fd = FinancialData(
|
||||
company_id=company_id,
|
||||
period_year=req.get("period_year"),
|
||||
period_month=req.get("period_month"),
|
||||
statement_type=req.get("statement_type", "balance_sheet"),
|
||||
data_json=req.get("data_json"),
|
||||
source=req.get("source"),
|
||||
)
|
||||
|
||||
validation = await validate_financial_data(db, company_id, fd.period_year, fd.period_month)
|
||||
fd.credibility_score = validation["credibility_score"]
|
||||
fd.validation_result = validation
|
||||
|
||||
db.add(fd)
|
||||
await db.flush()
|
||||
return success(data={"id": str(fd.id), "credibility_score": fd.credibility_score, "validation_result": validation}, message="创建成功")
|
||||
|
||||
|
||||
@router.get("/validate", response_model=ApiResponse[dict])
|
||||
async def validate_financial(
|
||||
company_id: str = Query(...),
|
||||
period_year: int = Query(...),
|
||||
period_month: int = Query(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""校验财务数据。"""
|
||||
result = await validate_financial_data(db, company_id, period_year, period_month)
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,148 @@
|
||||
"""创始人专属 API — 经营概览 + 自身健康度 + AI 副驾驶完整版。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
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.company import Company
|
||||
from app.models.health_score import HealthScore
|
||||
from app.models.report import MonthlyReport
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.founder_copilot import financing_planner, org_diagnostic, investor_comm_prep
|
||||
|
||||
router = APIRouter(prefix="/founder", tags=["founder"])
|
||||
|
||||
|
||||
@router.get("/overview", response_model=ApiResponse[dict])
|
||||
async def founder_overview(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""创始人经营概览。"""
|
||||
# 获取创始人关联的企业
|
||||
company_result = await db.execute(
|
||||
select(Company).where(Company.tenant_id == user.tenant_id).limit(1)
|
||||
)
|
||||
company = company_result.scalar_one_or_none()
|
||||
|
||||
if not company:
|
||||
return success(data={"message": "暂无关联企业"})
|
||||
|
||||
# 获取最新健康度
|
||||
health_result = await db.execute(
|
||||
select(HealthScore)
|
||||
.where(HealthScore.company_id == company.id)
|
||||
.order_by(HealthScore.calculated_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
health = health_result.scalar_one_or_none()
|
||||
|
||||
# 获取最新月报
|
||||
report_result = await db.execute(
|
||||
select(MonthlyReport)
|
||||
.where(MonthlyReport.company_id == company.id)
|
||||
.order_by(MonthlyReport.period_year.desc(), MonthlyReport.period_month.desc())
|
||||
.limit(1)
|
||||
)
|
||||
report = report_result.scalar_one_or_none()
|
||||
|
||||
return success(data={
|
||||
"company": {"id": str(company.id), "name": company.name, "industry": company.industry, "stage": company.stage},
|
||||
"health_score": {
|
||||
"total_score": health.total_score if health else None,
|
||||
"trend": health.trend if health else None,
|
||||
} if health else None,
|
||||
"latest_report": {
|
||||
"id": str(report.id),
|
||||
"period": f"{report.period_year}-{report.period_month:02d}",
|
||||
"status": report.status,
|
||||
} if report else None,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/health", response_model=ApiResponse[dict])
|
||||
async def founder_health(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""创始人查看自身健康度。"""
|
||||
company_result = await db.execute(
|
||||
select(Company).where(Company.tenant_id == user.tenant_id).limit(1)
|
||||
)
|
||||
company = company_result.scalar_one_or_none()
|
||||
if not company:
|
||||
return success(data=None)
|
||||
|
||||
result = await db.execute(
|
||||
select(HealthScore)
|
||||
.where(HealthScore.company_id == company.id)
|
||||
.order_by(HealthScore.calculated_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
health = result.scalar_one_or_none()
|
||||
if not health:
|
||||
return success(data=None)
|
||||
|
||||
return success(data={
|
||||
"total_score": health.total_score,
|
||||
"financial_score": health.financial_score,
|
||||
"operational_score": health.operational_score,
|
||||
"ai_commercial_score": health.ai_commercial_score,
|
||||
"ai_cost_score": health.ai_cost_score,
|
||||
"trend": health.trend,
|
||||
"recommendations": health.recommendations_json,
|
||||
})
|
||||
|
||||
|
||||
@router.post("/financing-plan", response_model=ApiResponse[dict])
|
||||
async def founder_financing_plan(
|
||||
req: dict,
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""AI 融资规划 — 节奏/估值/投资人画像。
|
||||
|
||||
Args:
|
||||
req: 包含 company_data 字段,描述企业当前融资情况
|
||||
|
||||
Returns:
|
||||
AI 生成的融资规划建议
|
||||
"""
|
||||
result = await financing_planner(req.get("company_data", ""))
|
||||
return success(data=result)
|
||||
|
||||
|
||||
@router.post("/org-diagnostic", response_model=ApiResponse[dict])
|
||||
async def founder_org_diagnostic(
|
||||
req: dict,
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""AI 组织诊断 — 团队结构/关键岗位风险/人才缺口。
|
||||
|
||||
Args:
|
||||
req: 包含 team_data 字段,描述团队当前情况
|
||||
|
||||
Returns:
|
||||
AI 生成的组织诊断报告
|
||||
"""
|
||||
result = await org_diagnostic(req.get("team_data", ""))
|
||||
return success(data=result)
|
||||
|
||||
|
||||
@router.post("/investor-comm-prep", response_model=ApiResponse[dict])
|
||||
async def founder_investor_comm_prep(
|
||||
req: dict,
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""AI 投资人沟通准备 — 董事会材料/投资人问答。
|
||||
|
||||
Args:
|
||||
req: 包含 board_context 字段,描述董事会/投资人会议背景
|
||||
|
||||
Returns:
|
||||
AI 生成的投资人沟通准备材料
|
||||
"""
|
||||
result = await investor_comm_prep(req.get("board_context", ""))
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,24 @@
|
||||
"""基金管理路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from app.core.dependencies import get_current_user
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.fund_strategy_analyzer import analyze_fund_strategy
|
||||
from app.services.lp_report_generator import generate_lp_report
|
||||
|
||||
router = APIRouter(prefix="/funds", tags=["funds"])
|
||||
|
||||
|
||||
@router.post("/analyze-strategy", response_model=ApiResponse[dict])
|
||||
async def analyze_strategy(req: dict, user: User = Depends(get_current_user)):
|
||||
"""基金策略分析。"""
|
||||
result = await analyze_fund_strategy(req.get("funds_data", ""))
|
||||
return success(data=result)
|
||||
|
||||
|
||||
@router.post("/lp-report", response_model=ApiResponse[str])
|
||||
async def generate_lp(req: dict, user: User = Depends(get_current_user)):
|
||||
"""LP 报告自动生成。"""
|
||||
result = await generate_lp_report(req.get("fund_data", ""), req.get("portfolio_summary", ""))
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,16 @@
|
||||
"""行业研究路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from app.core.dependencies import get_current_user
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.industry_research_agent import research_industry
|
||||
|
||||
router = APIRouter(prefix="/industry-research", tags=["industry-research"])
|
||||
|
||||
|
||||
@router.post("/research", response_model=ApiResponse[dict])
|
||||
async def research(req: dict, user: User = Depends(get_current_user)):
|
||||
"""AI 行业研究。"""
|
||||
result = await research_industry(req.get("industry", ""), req.get("companies", ""))
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,16 @@
|
||||
"""组合创新路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from app.core.dependencies import get_current_user
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.innovation_lab import discover_innovation_opportunities
|
||||
|
||||
router = APIRouter(prefix="/innovation", tags=["innovation"])
|
||||
|
||||
|
||||
@router.post("/discover", response_model=ApiResponse[list])
|
||||
async def discover_innovation(req: dict, user: User = Depends(get_current_user)):
|
||||
"""AI 发现组合创新机会。"""
|
||||
results = await discover_innovation_opportunities(req.get("portfolio_capabilities", ""))
|
||||
return success(data=results)
|
||||
@@ -0,0 +1,32 @@
|
||||
"""RAG 知识库路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
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
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.rag import semantic_search, build_context
|
||||
|
||||
router = APIRouter(prefix="/knowledge", tags=["knowledge"])
|
||||
|
||||
|
||||
@router.get("/search", response_model=ApiResponse[list])
|
||||
async def search_knowledge(
|
||||
q: str = Query(..., min_length=1),
|
||||
top_k: int = Query(default=5, ge=1, le=20),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""语义搜索知识库。"""
|
||||
results = await semantic_search(db, user.tenant_id, q, top_k)
|
||||
return success(data=results)
|
||||
|
||||
|
||||
@router.post("/context", response_model=ApiResponse[str])
|
||||
async def get_context(req: dict, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""构建 RAG 上下文。"""
|
||||
results = await semantic_search(db, user.tenant_id, req.get("query", ""))
|
||||
context = await build_context(results)
|
||||
return success(data=context)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""知识图谱路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from app.core.dependencies import get_current_user
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.knowledge_graph_builder import build_knowledge_graph, match_best_strategy
|
||||
|
||||
router = APIRouter(prefix="/knowledge-graph", tags=["knowledge-graph"])
|
||||
|
||||
|
||||
@router.post("/build", response_model=ApiResponse[dict])
|
||||
async def build_graph(req: dict, user: User = Depends(get_current_user)):
|
||||
"""构建知识图谱。"""
|
||||
result = await build_knowledge_graph(req.get("management_experiences", ""))
|
||||
return success(data=result)
|
||||
|
||||
|
||||
@router.post("/match-strategy", response_model=ApiResponse[dict])
|
||||
async def match_strategy(req: dict, user: User = Depends(get_current_user)):
|
||||
"""为新企业匹配最佳管理策略。"""
|
||||
result = await match_best_strategy(req.get("new_company_profile", ""), req.get("knowledge_graph", {}))
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,45 @@
|
||||
"""里程碑路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
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.milestone import MilestoneTree
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.milestone_agent import suggest_path_switch
|
||||
|
||||
router = APIRouter(prefix="/milestones", tags=["milestones"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[list])
|
||||
async def list_milestones(company_id: str = Query(...), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""获取里程碑树。"""
|
||||
result = await db.execute(
|
||||
select(MilestoneTree).where(MilestoneTree.company_id == company_id)
|
||||
)
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"company_id": str(i.company_id),
|
||||
"name": i.name,
|
||||
"parent_id": i.parent_id,
|
||||
"is_current": i.is_current,
|
||||
"status": i.status,
|
||||
"target_date": i.target_date.isoformat() if i.target_date else None,
|
||||
"actual_date": i.actual_date.isoformat() if i.actual_date else None,
|
||||
"description": i.description,
|
||||
"ai_analysis": i.ai_analysis,
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("/suggest-switch", response_model=ApiResponse[dict])
|
||||
async def suggest_switch(req: dict, user: User = Depends(get_current_user)):
|
||||
"""AI 建议路径切换。"""
|
||||
result = await suggest_path_switch(req.get("milestone_context", ""), req.get("env_changes", ""))
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,42 @@
|
||||
"""行为助推路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
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.nudge import NudgeRecord
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.nudge_agent import select_nudge_strategy
|
||||
|
||||
router = APIRouter(prefix="/nudges", tags=["nudges"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[list])
|
||||
async def list_nudges(company_id: str | None = Query(default=None), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""获取助推记录列表。"""
|
||||
query = select(NudgeRecord)
|
||||
if company_id:
|
||||
query = query.where(NudgeRecord.company_id == company_id)
|
||||
result = await db.execute(query.order_by(NudgeRecord.created_at.desc()))
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"company_id": str(i.company_id),
|
||||
"nudge_type": i.nudge_type,
|
||||
"message": i.message,
|
||||
"accepted": i.accepted,
|
||||
"effect_result": i.effect_result,
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("/select", response_model=ApiResponse[dict])
|
||||
async def select_nudge(req: dict, user: User = Depends(get_current_user)):
|
||||
"""AI 选择助推策略。"""
|
||||
result = await select_nudge_strategy(req.get("context", ""))
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,59 @@
|
||||
"""OKR 路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
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.okr import OKR
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.okr_agent import track_okr_progress
|
||||
|
||||
router = APIRouter(prefix="/okrs", tags=["okrs"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[list])
|
||||
async def list_okrs(company_id: str | None = Query(default=None), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""获取 OKR 列表。"""
|
||||
query = select(OKR)
|
||||
if company_id:
|
||||
query = query.where(OKR.company_id == company_id)
|
||||
result = await db.execute(query.order_by(OKR.quarter.desc()))
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"company_id": str(i.company_id),
|
||||
"quarter": i.quarter,
|
||||
"objective": i.objective,
|
||||
"key_results": i.key_results,
|
||||
"alignment_score": i.alignment_score,
|
||||
"deviation_alerts": i.deviation_alerts,
|
||||
"review_notes": i.review_notes,
|
||||
"status": i.status,
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("", response_model=ApiResponse[dict], status_code=status.HTTP_201_CREATED)
|
||||
async def create_okr(req: dict, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""创建 OKR。"""
|
||||
okr = OKR(
|
||||
company_id=req.get("company_id"),
|
||||
quarter=req.get("quarter"),
|
||||
objective=req.get("objective"),
|
||||
key_results=req.get("key_results"),
|
||||
)
|
||||
db.add(okr)
|
||||
await db.flush()
|
||||
return success(data={"id": str(okr.id)}, message="创建成功")
|
||||
|
||||
|
||||
@router.post("/{okr_id}/track", response_model=ApiResponse[dict])
|
||||
async def track_okr(okr_id: str, req: dict, user: User = Depends(get_current_user)):
|
||||
"""AI 追踪 KR 进展。"""
|
||||
result = await track_okr_progress(req.get("key_results", []))
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Peer Learning Circles 路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
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.peer_circle import PeerLearningCircle
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.peer_matching import match_founders
|
||||
|
||||
router = APIRouter(prefix="/peer-circles", tags=["peer-circles"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[list])
|
||||
async def list_circles(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""获取 Peer Learning Circle 列表。"""
|
||||
result = await db.execute(
|
||||
select(PeerLearningCircle).where(PeerLearningCircle.tenant_id == user.tenant_id)
|
||||
)
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"topic": i.topic,
|
||||
"description": i.description,
|
||||
"members": i.members,
|
||||
"discussion_framework": i.discussion_framework,
|
||||
"conclusions": i.conclusions,
|
||||
"action_commitments": i.action_commitments,
|
||||
"status": i.status,
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("/match", response_model=ApiResponse[dict])
|
||||
async def match_peer_circle(req: dict, user: User = Depends(get_current_user)):
|
||||
"""AI 匹配创始人。"""
|
||||
result = await match_founders(req.get("founders_context", ""))
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,24 @@
|
||||
"""组合管理路由 — 再平衡 + Monte Carlo。"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from app.core.dependencies import get_current_user
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.portfolio_rebalancer import rebalance_portfolio
|
||||
from app.services.monte_carlo import simulate_portfolio
|
||||
|
||||
router = APIRouter(prefix="/portfolio", tags=["portfolio"])
|
||||
|
||||
|
||||
@router.post("/rebalance", response_model=ApiResponse[dict])
|
||||
async def rebalance(req: dict, user: User = Depends(get_current_user)):
|
||||
"""组合再平衡建议。"""
|
||||
result = rebalance_portfolio(req.get("company_returns", []))
|
||||
return success(data=result)
|
||||
|
||||
|
||||
@router.post("/monte-carlo", response_model=ApiResponse[dict])
|
||||
async def monte_carlo(req: dict, user: User = Depends(get_current_user)):
|
||||
"""Monte Carlo 模拟。"""
|
||||
result = await simulate_portfolio(req.get("company_returns", []), req.get("iterations", 10000))
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Pre-mortem + Red Team 路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
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.pre_mortem import PreMortemRecord, RedTeamRecord
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.pre_mortem_agent import run_pre_mortem
|
||||
from app.services.red_team_agent import run_red_team
|
||||
|
||||
router_pre_mortem = APIRouter(prefix="/pre-mortems", tags=["pre-mortems"])
|
||||
router_red_team = APIRouter(prefix="/red-teams", tags=["red-teams"])
|
||||
|
||||
|
||||
@router_pre_mortem.get("", response_model=ApiResponse[list])
|
||||
async def list_pre_mortems(company_id: str | None = Query(default=None), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""获取 Pre-mortem 列表。"""
|
||||
query = select(PreMortemRecord)
|
||||
if company_id:
|
||||
query = query.where(PreMortemRecord.company_id == company_id)
|
||||
result = await db.execute(query.order_by(PreMortemRecord.created_at.desc()))
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{"id": str(i.id), "company_id": str(i.company_id), "decision_context": i.decision_context, "failure_paths": i.failure_paths, "risk_checklist": i.risk_checklist, "mitigations": i.mitigations}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router_pre_mortem.post("/run", response_model=ApiResponse[dict])
|
||||
async def run_pre_mortem_analysis(req: dict, user: User = Depends(get_current_user)):
|
||||
"""AI Pre-mortem 失败推演。"""
|
||||
result = await run_pre_mortem(req.get("decision_context", ""))
|
||||
return success(data=result)
|
||||
|
||||
|
||||
@router_red_team.get("", response_model=ApiResponse[list])
|
||||
async def list_red_teams(company_id: str | None = Query(default=None), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""获取 Red Team 列表。"""
|
||||
query = select(RedTeamRecord)
|
||||
if company_id:
|
||||
query = query.where(RedTeamRecord.company_id == company_id)
|
||||
result = await db.execute(query.order_by(RedTeamRecord.created_at.desc()))
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{"id": str(i.id), "company_id": str(i.company_id), "perspective": i.perspective, "analysis": i.analysis, "vulnerabilities": i.vulnerabilities, "counterarguments": i.counterarguments}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router_red_team.post("/run", response_model=ApiResponse[dict])
|
||||
async def run_red_team_analysis(req: dict, user: User = Depends(get_current_user)):
|
||||
"""AI Red Team 对抗分析。"""
|
||||
result = await run_red_team(req.get("company_context", ""), req.get("perspective", "competitor"))
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,43 @@
|
||||
"""产品竞争力诊断路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
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.product_diagnostic import ProductDiagnostic
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.services.product_diagnostic_agent import diagnose_product
|
||||
|
||||
router = APIRouter(prefix="/product-diagnostics", tags=["product-diagnostics"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[list])
|
||||
async def list_diagnostics(company_id: str | None = Query(default=None), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""获取产品诊断列表。"""
|
||||
query = select(ProductDiagnostic)
|
||||
if company_id:
|
||||
query = query.where(ProductDiagnostic.company_id == company_id)
|
||||
result = await db.execute(query.order_by(ProductDiagnostic.created_at.desc()))
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{
|
||||
"id": str(i.id),
|
||||
"company_id": str(i.company_id),
|
||||
"product_name": i.product_name,
|
||||
"dimensions": i.dimensions,
|
||||
"heatmap_data": i.heatmap_data,
|
||||
"competitors": i.competitors,
|
||||
"roadmap_suggestions": i.roadmap_suggestions,
|
||||
}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("/diagnose", response_model=ApiResponse[dict])
|
||||
async def diagnose(req: dict, user: User = Depends(get_current_user)):
|
||||
"""AI 产品竞争力诊断。"""
|
||||
result = await diagnose_product(req.get("product_info", ""), req.get("competitor_info", ""))
|
||||
return success(data=result)
|
||||
@@ -0,0 +1,72 @@
|
||||
"""多主体画像路由。"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
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.profile import FirmProfile, FundProfile, ManagerProfile
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
|
||||
router = APIRouter(prefix="/profiles", tags=["profiles"])
|
||||
|
||||
|
||||
@router.get("/firms", response_model=ApiResponse[list])
|
||||
async def list_firms(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取投资机构列表。"""
|
||||
result = await db.execute(
|
||||
select(FirmProfile).where(FirmProfile.tenant_id == user.tenant_id)
|
||||
)
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{"id": str(i.id), "name": i.name, "focus_areas": i.focus_areas, "stage_preference": i.stage_preference, "description": i.description}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.post("/firms", response_model=ApiResponse[dict], status_code=status.HTTP_201_CREATED)
|
||||
async def create_firm(req: dict, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""创建投资机构画像。"""
|
||||
firm = FirmProfile(
|
||||
tenant_id=user.tenant_id,
|
||||
name=req.get("name"),
|
||||
focus_areas=req.get("focus_areas"),
|
||||
stage_preference=req.get("stage_preference"),
|
||||
description=req.get("description"),
|
||||
)
|
||||
db.add(firm)
|
||||
await db.flush()
|
||||
return success(data={"id": str(firm.id)}, message="创建成功")
|
||||
|
||||
|
||||
@router.get("/funds", response_model=ApiResponse[list])
|
||||
async def list_funds(firm_id: str | None = Query(default=None), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""获取基金列表。"""
|
||||
query = select(FundProfile)
|
||||
if firm_id:
|
||||
query = query.where(FundProfile.firm_id == firm_id)
|
||||
result = await db.execute(query)
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{"id": str(i.id), "firm_id": str(i.firm_id), "name": i.name, "fund_size": i.fund_size, "vintage_year": i.vintage_year, "strategy": i.strategy}
|
||||
for i in items
|
||||
])
|
||||
|
||||
|
||||
@router.get("/managers", response_model=ApiResponse[list])
|
||||
async def list_managers(firm_id: str | None = Query(default=None), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
||||
"""获取投资经理列表。"""
|
||||
query = select(ManagerProfile)
|
||||
if firm_id:
|
||||
query = query.where(ManagerProfile.firm_id == firm_id)
|
||||
result = await db.execute(query)
|
||||
items = result.scalars().all()
|
||||
return success(data=[
|
||||
{"id": str(i.id), "firm_id": str(i.firm_id), "name": i.name, "focus_areas": i.focus_areas, "portfolio_count": i.portfolio_count}
|
||||
for i in items
|
||||
])
|
||||
@@ -0,0 +1,415 @@
|
||||
"""月报路由:CRUD + 提交 + AI 解析(SSE 流式)。"""
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy import func, 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.company import Company
|
||||
from app.models.health_score import HealthScore
|
||||
from app.models.report import MonthlyReport
|
||||
from app.models.risk import RiskEvent
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success
|
||||
from app.schemas.report import (
|
||||
MonthlyReportCreate,
|
||||
MonthlyReportListResponse,
|
||||
MonthlyReportResponse,
|
||||
MonthlyReportUpdate,
|
||||
)
|
||||
from app.services.ai_parser import parse_report
|
||||
from app.services.health_calculator import calculate_health_score, determine_trend
|
||||
from app.services.report_tracker import compute_timeliness
|
||||
from app.services.risk_engine import detect_risks
|
||||
from app.services.file_parser import parse_file
|
||||
|
||||
router = APIRouter(prefix="/reports", tags=["reports"])
|
||||
|
||||
|
||||
async def _get_company_or_404(db: AsyncSession, company_id: str, tenant_id: str) -> Company:
|
||||
"""验证企业属于当前租户。"""
|
||||
result = await db.execute(
|
||||
select(Company).where(Company.id == company_id, Company.tenant_id == tenant_id)
|
||||
)
|
||||
company = result.scalar_one_or_none()
|
||||
if not company:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="企业不存在")
|
||||
return company
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[MonthlyReportListResponse])
|
||||
async def list_reports(
|
||||
company_id: str | None = Query(default=None, description="按企业筛选"),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取月报列表。"""
|
||||
# 获取当前租户的企业 ID 集合
|
||||
company_query = select(Company.id).where(Company.tenant_id == user.tenant_id)
|
||||
if company_id:
|
||||
company_query = company_query.where(Company.id == company_id)
|
||||
|
||||
query = select(MonthlyReport).where(
|
||||
MonthlyReport.company_id.in_(company_query)
|
||||
)
|
||||
|
||||
count_query = select(func.count()).select_from(query.subquery())
|
||||
total_result = await db.execute(count_query)
|
||||
total = total_result.scalar_one()
|
||||
|
||||
offset = (page - 1) * page_size
|
||||
query = query.order_by(MonthlyReport.created_at.desc()).offset(offset).limit(page_size)
|
||||
result = await db.execute(query)
|
||||
reports = result.scalars().all()
|
||||
|
||||
return success(
|
||||
data=MonthlyReportListResponse(
|
||||
items=[MonthlyReportResponse.model_validate(r, from_attributes=True) for r in reports],
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{report_id}", response_model=ApiResponse[MonthlyReportResponse])
|
||||
async def get_report(
|
||||
report_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取月报详情。"""
|
||||
result = await db.execute(
|
||||
select(MonthlyReport)
|
||||
.join(Company, MonthlyReport.company_id == Company.id)
|
||||
.where(MonthlyReport.id == report_id, Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
report = result.scalar_one_or_none()
|
||||
if not report:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="月报不存在")
|
||||
return success(data=MonthlyReportResponse.model_validate(report, from_attributes=True))
|
||||
|
||||
|
||||
@router.post("", response_model=ApiResponse[MonthlyReportResponse], status_code=status.HTTP_201_CREATED)
|
||||
async def create_report(
|
||||
req: MonthlyReportCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""创建月报。"""
|
||||
await _get_company_or_404(db, req.company_id, user.tenant_id)
|
||||
|
||||
# 检查同企业同年月是否已有月报
|
||||
existing = await db.execute(
|
||||
select(MonthlyReport).where(
|
||||
MonthlyReport.company_id == req.company_id,
|
||||
MonthlyReport.period_year == req.period_year,
|
||||
MonthlyReport.period_month == req.period_month,
|
||||
)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"{req.period_year}年{req.period_month}月月报已存在",
|
||||
)
|
||||
|
||||
report = MonthlyReport(
|
||||
company_id=req.company_id,
|
||||
period_year=req.period_year,
|
||||
period_month=req.period_month,
|
||||
raw_content=req.raw_content,
|
||||
status="draft",
|
||||
)
|
||||
db.add(report)
|
||||
await db.flush()
|
||||
return success(
|
||||
data=MonthlyReportResponse.model_validate(report, from_attributes=True),
|
||||
message="创建成功",
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{report_id}", response_model=ApiResponse[MonthlyReportResponse])
|
||||
async def update_report(
|
||||
report_id: str,
|
||||
req: MonthlyReportUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""更新月报。"""
|
||||
result = await db.execute(
|
||||
select(MonthlyReport)
|
||||
.join(Company, MonthlyReport.company_id == Company.id)
|
||||
.where(MonthlyReport.id == report_id, Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
report = result.scalar_one_or_none()
|
||||
if not report:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="月报不存在")
|
||||
|
||||
update_data = req.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(report, key, value)
|
||||
|
||||
await db.flush()
|
||||
return success(
|
||||
data=MonthlyReportResponse.model_validate(report, from_attributes=True),
|
||||
message="更新成功",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{report_id}/submit", response_model=ApiResponse[MonthlyReportResponse])
|
||||
async def submit_report(
|
||||
report_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""提交月报(状态从 draft → submitted)。"""
|
||||
result = await db.execute(
|
||||
select(MonthlyReport)
|
||||
.join(Company, MonthlyReport.company_id == Company.id)
|
||||
.where(MonthlyReport.id == report_id, Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
report = result.scalar_one_or_none()
|
||||
if not report:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="月报不存在")
|
||||
|
||||
if report.status != "draft":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"当前状态为 {report.status},无法提交",
|
||||
)
|
||||
|
||||
report.status = "submitted"
|
||||
report.submitted_by = user.id
|
||||
report.submitted_at = datetime.now(timezone.utc)
|
||||
await db.flush()
|
||||
return success(
|
||||
data=MonthlyReportResponse.model_validate(report, from_attributes=True),
|
||||
message="提交成功",
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{report_id}", response_model=ApiResponse[None])
|
||||
async def delete_report(
|
||||
report_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""删除月报。"""
|
||||
result = await db.execute(
|
||||
select(MonthlyReport)
|
||||
.join(Company, MonthlyReport.company_id == Company.id)
|
||||
.where(MonthlyReport.id == report_id, Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
report = result.scalar_one_or_none()
|
||||
if not report:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="月报不存在")
|
||||
|
||||
await db.delete(report)
|
||||
return success(message="删除成功")
|
||||
|
||||
|
||||
@router.post("/{report_id}/parse")
|
||||
async def parse_report_stream(
|
||||
report_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""AI 解析月报(SSE 流式输出)。
|
||||
|
||||
流式返回解析过程中的 token,最后返回完整结构化结果。
|
||||
解析完成后自动计算健康度评分并检测风险事件。
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(MonthlyReport)
|
||||
.join(Company, MonthlyReport.company_id == Company.id)
|
||||
.where(MonthlyReport.id == report_id, Company.tenant_id == user.tenant_id)
|
||||
)
|
||||
report = result.scalar_one_or_none()
|
||||
if not report:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="月报不存在")
|
||||
|
||||
async def event_stream():
|
||||
"""SSE 事件流。"""
|
||||
try:
|
||||
# 阶段 1:流式输出 AI 解析
|
||||
yield f"data: {json.dumps({'type': 'status', 'message': 'AI 解析中...'})}\n\n"
|
||||
|
||||
from app.services.llm_client import llm_client
|
||||
|
||||
system_prompt = """你是投后管理领域的专业分析师。请分析以下企业月报内容,提取结构化信息。
|
||||
|
||||
输出 JSON 格式如下:
|
||||
{
|
||||
"structured_data": {
|
||||
"revenue": {"value": "", "unit": "万元", "yoy_change": "", "note": ""},
|
||||
"cash_balance": {"value": "", "unit": "万元", "runway_months": 0, "note": ""},
|
||||
"burn_rate": {"value": "", "unit": "万元/月", "trend": "up/stable/down", "note": ""},
|
||||
"headcount": {"total": 0, "new_hires": 0, "departures": 0, "note": ""},
|
||||
"key_metrics": [{"name": "", "value": "", "change": "", "note": ""}]
|
||||
},
|
||||
"ai_summary": "一段 100-200 字的月报摘要",
|
||||
"ai_concerns": {
|
||||
"items": [
|
||||
{"category": "financial/operational/org/ai_specific", "severity": "low/medium/high", "description": ""}
|
||||
],
|
||||
"highlights": ["本期亮点1", "本期亮点2"]
|
||||
}
|
||||
}
|
||||
|
||||
严格输出 JSON,不要包含 markdown 代码块标记。"""
|
||||
|
||||
user_prompt = f"请分析以下 {report.period_year}年{report.period_month}月 月报内容:\n\n<<<USER_INPUT>>>\n{report.raw_content or '无内容'}\n<<<END_USER_INPUT>>>"
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt},
|
||||
]
|
||||
|
||||
# 流式收集
|
||||
collected = []
|
||||
async for token in llm_client.chat_json_stream(messages, temperature=0.3, max_tokens=2000):
|
||||
collected.append(token)
|
||||
yield f"data: {json.dumps({'type': 'token', 'content': token})}\n\n"
|
||||
|
||||
# 解析完整 JSON
|
||||
full_text = "".join(collected).strip()
|
||||
if full_text.startswith("```"):
|
||||
full_text = full_text.split("\n", 1)[1] if "\n" in full_text else full_text[3:]
|
||||
if full_text.endswith("```"):
|
||||
full_text = full_text[:-3]
|
||||
full_text = full_text.strip()
|
||||
|
||||
parsed = json.loads(full_text)
|
||||
|
||||
# 阶段 2:保存解析结果
|
||||
report.structured_data = parsed.get("structured_data", {})
|
||||
report.ai_summary = parsed.get("ai_summary", "")
|
||||
report.ai_concerns = parsed.get("ai_concerns", {})
|
||||
report.status = "ai_parsed"
|
||||
await db.flush()
|
||||
|
||||
yield f"data: {json.dumps({'type': 'parsed', 'data': parsed})}\n\n"
|
||||
|
||||
# 阶段 3:计算健康度评分
|
||||
yield f"data: {json.dumps({'type': 'status', 'message': '计算健康度评分...'})}\n\n"
|
||||
|
||||
scores = calculate_health_score(parsed.get("structured_data", {}))
|
||||
|
||||
# 查询上期评分判断趋势
|
||||
prev_result = await db.execute(
|
||||
select(HealthScore)
|
||||
.where(HealthScore.company_id == report.company_id)
|
||||
.order_by(HealthScore.calculated_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
prev_score = prev_result.scalar_one_or_none()
|
||||
prev_total = prev_score.total_score if prev_score else None
|
||||
trend = determine_trend(scores["total_score"], prev_total)
|
||||
|
||||
health = HealthScore(
|
||||
company_id=report.company_id,
|
||||
total_score=scores["total_score"],
|
||||
financial_score=scores["financial_score"],
|
||||
operational_score=scores["operational_score"],
|
||||
ai_commercial_score=scores["ai_commercial_score"],
|
||||
ai_cost_score=scores["ai_cost_score"],
|
||||
trend=trend,
|
||||
evidence_json={"report_id": report.id, "period": f"{report.period_year}-{report.period_month}"},
|
||||
)
|
||||
db.add(health)
|
||||
await db.flush()
|
||||
|
||||
yield f"data: {json.dumps({'type': 'health_score', 'data': scores, 'trend': trend})}\n\n"
|
||||
|
||||
# 阶段 4:风险自动检测
|
||||
yield f"data: {json.dumps({'type': 'status', 'message': '检测风险事件...'})}\n\n"
|
||||
|
||||
risks = detect_risks(parsed.get("structured_data", {}), report.company_id)
|
||||
created_risks = []
|
||||
for risk_data in risks:
|
||||
risk = RiskEvent(
|
||||
company_id=risk_data["company_id"],
|
||||
type=risk_data["type"],
|
||||
severity=risk_data["severity"],
|
||||
title=risk_data["title"],
|
||||
description=risk_data["description"],
|
||||
suggested_action=risk_data["suggested_action"],
|
||||
status="open",
|
||||
)
|
||||
db.add(risk)
|
||||
await db.flush()
|
||||
created_risks.append({
|
||||
"id": risk.id,
|
||||
"title": risk_data["title"],
|
||||
"severity": risk_data["severity"],
|
||||
"type": risk_data["type"],
|
||||
})
|
||||
|
||||
yield f"data: {json.dumps({'type': 'risks', 'data': created_risks})}\n\n"
|
||||
|
||||
# 完成
|
||||
yield f"data: {json.dumps({'type': 'done', 'message': '解析完成'})}\n\n"
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
yield f"data: {json.dumps({'type': 'error', 'message': f'JSON 解析失败: {e}'})}\n\n"
|
||||
except Exception as e:
|
||||
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
event_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/timeliness", response_model=ApiResponse[list])
|
||||
async def get_timeliness(
|
||||
company_id: str | None = Query(default=None),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取月报提交及时性和数据质量评分。"""
|
||||
items = await compute_timeliness(db, user.tenant_id, company_id)
|
||||
return success(data=items)
|
||||
|
||||
|
||||
@router.post("/upload", response_model=ApiResponse[dict])
|
||||
async def upload_report_file(
|
||||
file: UploadFile = File(...),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""上传月报文件 — 自动解析提取文本内容。
|
||||
|
||||
支持 .xlsx、.pdf、.txt、.md、.csv 格式。
|
||||
"""
|
||||
if not file.filename:
|
||||
raise HTTPException(status_code=400, detail="文件名不能为空")
|
||||
|
||||
allowed_extensions = {".xlsx", ".xls", ".pdf", ".txt", ".md", ".csv"}
|
||||
ext = file.filename.rsplit(".", 1)[-1].lower() if "." in file.filename else ""
|
||||
if f".{ext}" not in allowed_extensions:
|
||||
raise HTTPException(status_code=400, detail=f"不支持的文件格式: .{ext}")
|
||||
|
||||
content = await file.read()
|
||||
if len(content) > 10 * 1024 * 1024:
|
||||
raise HTTPException(status_code=400, detail="文件大小不能超过 10MB")
|
||||
|
||||
extracted_text = await parse_file(content, file.filename)
|
||||
|
||||
return success(data={
|
||||
"filename": file.filename,
|
||||
"file_type": ext,
|
||||
"extracted_text": extracted_text[:10000],
|
||||
"char_count": len(extracted_text),
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user