Compare commits
2 Commits
ca60410966
...
230f180dea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
230f180dea | ||
|
|
3d0fd6f704 |
23
.gitignore
vendored
23
.gitignore
vendored
@ -1,3 +1,26 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
.pytest_cache/
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
frontend/node_modules/
|
||||
services/**/node_modules/
|
||||
|
||||
# Env & local
|
||||
.env
|
||||
.env.*
|
||||
.DS_Store
|
||||
|
||||
# Build artifacts
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Binaries
|
||||
portwardenc-amd64
|
||||
|
||||
# ===== 通用文件 =====
|
||||
# 操作系统生成的文件
|
||||
.DS_Store
|
||||
|
||||
23
backend/Dockerfile
Normal file
23
backend/Dockerfile
Normal file
@ -0,0 +1,23 @@
|
||||
# syntax=docker/dockerfile:1.6
|
||||
|
||||
FROM python:3.11-slim AS base
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=1
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
# 仅复制依赖文件,提升缓存命中率
|
||||
COPY backend/requirements.txt ./backend/requirements.txt
|
||||
RUN pip install --upgrade pip && \
|
||||
pip install --no-cache-dir -r backend/requirements.txt
|
||||
|
||||
# 运行时通过挂载卷提供源码;这里仅创建目录以便于容器内路径存在
|
||||
RUN mkdir -p /workspace/backend
|
||||
|
||||
WORKDIR /workspace/backend
|
||||
|
||||
# 缺省入口由 docker-compose 提供
|
||||
|
||||
|
||||
@ -23,6 +23,9 @@ class Settings(BaseSettings):
|
||||
GEMINI_API_KEY: Optional[str] = None
|
||||
TUSHARE_TOKEN: Optional[str] = None
|
||||
|
||||
# Microservices
|
||||
CONFIG_SERVICE_BASE_URL: str = "http://config-service:7000/api/v1"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
|
||||
@ -13,6 +13,7 @@ from sqlalchemy.future import select
|
||||
|
||||
from app.models.system_config import SystemConfig
|
||||
from app.schemas.config import ConfigResponse, ConfigUpdateRequest, DatabaseConfig, NewApiConfig, DataSourceConfig, ConfigTestResponse
|
||||
from app.core.config import settings
|
||||
|
||||
class ConfigManager:
|
||||
"""Manages system configuration by merging a static JSON file with dynamic settings from the database."""
|
||||
@ -28,14 +29,24 @@ class ConfigManager:
|
||||
else:
|
||||
self.config_path = config_path
|
||||
|
||||
def _load_base_config_from_file(self) -> Dict[str, Any]:
|
||||
"""Loads the base configuration from the JSON file."""
|
||||
if not os.path.exists(self.config_path):
|
||||
return {}
|
||||
async def _fetch_base_config_from_service(self) -> Dict[str, Any]:
|
||||
"""Fetch base configuration from config-service via HTTP."""
|
||||
base_url = settings.CONFIG_SERVICE_BASE_URL.rstrip("/")
|
||||
url = f"{base_url}/system"
|
||||
try:
|
||||
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (IOError, json.JSONDecodeError):
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.get(url)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
return data if isinstance(data, dict) else {}
|
||||
except Exception:
|
||||
# 为保持兼容性(阶段性迁移),在失败时回退到本地文件读取
|
||||
if os.path.exists(self.config_path):
|
||||
try:
|
||||
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
async def _load_dynamic_config_from_db(self) -> Dict[str, Any]:
|
||||
@ -64,7 +75,7 @@ class ConfigManager:
|
||||
|
||||
async def get_config(self) -> ConfigResponse:
|
||||
"""Gets the final, merged configuration."""
|
||||
base_config = self._load_base_config_from_file()
|
||||
base_config = await self._fetch_base_config_from_service()
|
||||
db_config = await self._load_dynamic_config_from_db()
|
||||
|
||||
merged_config = self._merge_configs(base_config, db_config)
|
||||
|
||||
@ -28,3 +28,5 @@ module.exports = {
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
|
||||
89
docker-compose.yml
Normal file
89
docker-compose.yml
Normal file
@ -0,0 +1,89 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
postgres-db:
|
||||
image: postgres:16-alpine
|
||||
container_name: fundamental-postgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: fundamental
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d fundamental"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
ports:
|
||||
- "15432:5432"
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: backend/Dockerfile
|
||||
container_name: fundamental-backend
|
||||
working_dir: /workspace/backend
|
||||
command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
environment:
|
||||
PYTHONDONTWRITEBYTECODE: "1"
|
||||
PYTHONUNBUFFERED: "1"
|
||||
# SQLAlchemy async driver
|
||||
DATABASE_URL: postgresql+asyncpg://postgres:postgres@postgres-db:5432/fundamental
|
||||
# Config service base URL
|
||||
CONFIG_SERVICE_BASE_URL: http://config-service:7000/api/v1
|
||||
volumes:
|
||||
# 挂载整个项目,确保后端代码中对项目根目录的相对路径(如 config/)仍然有效
|
||||
- ./:/workspace
|
||||
ports:
|
||||
- "18000:8000"
|
||||
depends_on:
|
||||
postgres-db:
|
||||
condition: service_healthy
|
||||
config-service:
|
||||
condition: service_started
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: frontend/Dockerfile
|
||||
container_name: fundamental-frontend
|
||||
working_dir: /workspace/frontend
|
||||
command: npm run dev
|
||||
environment:
|
||||
# 让 Next 的 API 路由代理到后端容器
|
||||
NEXT_PUBLIC_BACKEND_URL: http://backend:8000/api
|
||||
# Prisma 直连数据库(与后端共用同一库)
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres-db:5432/fundamental?schema=public
|
||||
NODE_ENV: development
|
||||
NEXT_TELEMETRY_DISABLED: "1"
|
||||
volumes:
|
||||
- ./:/workspace
|
||||
# 隔离 node_modules,避免与宿主机冲突
|
||||
- frontend_node_modules:/workspace/frontend/node_modules
|
||||
ports:
|
||||
- "13001:3001"
|
||||
depends_on:
|
||||
- backend
|
||||
- postgres-db
|
||||
- config-service
|
||||
|
||||
config-service:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: services/config-service/Dockerfile
|
||||
container_name: fundamental-config-service
|
||||
working_dir: /workspace/services/config-service
|
||||
command: uvicorn app.main:app --host 0.0.0.0 --port 7000
|
||||
environment:
|
||||
PROJECT_ROOT: /workspace
|
||||
volumes:
|
||||
- ./:/workspace
|
||||
ports:
|
||||
- "17000:7000"
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
frontend_node_modules:
|
||||
|
||||
|
||||
166
docs/microservice_refactoring_plan.md
Normal file
166
docs/microservice_refactoring_plan.md
Normal file
@ -0,0 +1,166 @@
|
||||
# 微服务架构重构计划
|
||||
|
||||
## 1. 引言
|
||||
|
||||
### 1.1. 文档目的
|
||||
|
||||
本文档旨在为“基本面选股系统”从单体架构向微服务架构的演进提供一个全面的设计蓝图和分阶段的实施路线图。它将详细阐述目标架构、服务划分、技术栈选型以及具体的迁移步骤,作为后续开发工作的核心指导文件。
|
||||
|
||||
### 1.2. 重构目标与收益
|
||||
|
||||
当前系统采用的是经典的前后端分离的单体架构。为了应对未来更复杂的需求、提升系统的可维护性、可扩展性并实现关键模块的独立部署与扩缩容,我们决定将其重构为微服务架构。
|
||||
|
||||
主要收益包括:
|
||||
- **高内聚、低耦合**: 每个服务只关注单一业务职责,易于理解和维护。
|
||||
- **独立部署与交付**: 可以对单个服务进行修改、测试和部署,而不影响整个系统,加快迭代速度。
|
||||
- **技术异构性**: 未来可以为不同的服务选择最适合的技术栈。
|
||||
- **弹性伸缩**: 可以根据负载情况,对高负荷的服务(如AI分析服务)进行独立扩容。
|
||||
- **故障隔离**: 单个服务的故障不会导致整个系统崩溃。
|
||||
|
||||
## 2. 目标架构设计
|
||||
|
||||
我们将采用以 `API网关` 为核心的微服务架构模式。前端应用将通过网关与后端一系列独立的微服务进行通信。
|
||||
|
||||

|
||||
|
||||
### 2.1. 服务划分 (Service Breakdown)
|
||||
|
||||
现有的后端应用将被拆分为以下几个核心微服务:
|
||||
|
||||
| 服务名称 | 容器名 (`docker-compose`) | 核心职责 |
|
||||
| :--- | :--- | :--- |
|
||||
| **前端应用** | `frontend-web` | **(保持不变)** Next.js UI,负责用户交互。 |
|
||||
| **API网关** | `api-gateway` | **(新增)** 系统统一入口。负责请求路由、认证、限流、日志聚合。将前端请求转发到正确的内部服务。 |
|
||||
| **报告编排器** | `report-orchestrator` | **(由原后端演变)** 负责报告生成的业务工作流。接收请求,调用数据、分析等服务,编排整个流程。 |
|
||||
| **数据聚合服务**| `data-aggregator` | **(新增)** 封装所有对第三方数据源(Tushare, Finnhub等)的API调用,并提供统一的数据接口,内置缓存逻辑。 |
|
||||
| **AI分析服务** | `analysis-service` | **(新增)** 专门负责与大语言模型(Gemini)交互。将其独立出来便于未来单独扩容或部署到GPU服务器。 |
|
||||
| **配置服务** | `config-service` | **(新增)** 集中管理并提供所有动态配置(API密钥、Prompt模板等),实现配置的动态更新与统一管理。 |
|
||||
| **数据库** | `postgres-db` | **(保持不变)** 独立的PostgreSQL数据库容器,为所有服务提供持久化存储。 |
|
||||
|
||||
### 2.2. 技术栈与开发环境
|
||||
|
||||
- **容器化**: `Docker`
|
||||
- **服务编排**: `Docker Compose`
|
||||
- **开发环境管理**: `Tilt`
|
||||
- **服务间通信**: 同步通信采用轻量级的 `RESTful API (HTTP)`。对于长任务,未来可引入 `RabbitMQ` 或 `Redis Stream` 等消息队列实现异步通信。
|
||||
|
||||
### 2.3. 项目根目录清洁化 (Root Directory Cleanup)
|
||||
|
||||
根据约定,项目根目录应保持整洁,只存放与整个项目和微服务编排直接相关的顶级文件和目录。所有业务代码、独立应用的配置和脚本工具都应被归纳到合适的子目录中。
|
||||
|
||||
- **`services/` 目录**: 所有微服务(包括 `frontend` 和 `backend`)的代码都将迁移至此目录下。
|
||||
- **`deployment/` 目录**: 用于存放与生产环境部署相关的配置文件(例如,`pm2.config.js`)。
|
||||
- **`scripts/` 目录**: 用于存放各类开发、构建、工具类脚本(例如,`dev.py`, 根目录的 `package.json` 等)。
|
||||
- **`.gitignore`**: 应添加规则以忽略开发者个人工具和二进制文件(例如,`portwardenc-amd64`)。
|
||||
|
||||
## 3. 分阶段实施计划
|
||||
|
||||
我们将采用增量、迭代的方式进行重构,确保每一步都是可验证、低风险的。
|
||||
|
||||
### 阶段 0: 容器化现有单体应用
|
||||
|
||||
**目标**: 在不修改任何业务代码的前提下,将现有的 `frontend` 和 `backend` 应用容器化,并使用 `docker-compose` 和 `Tilt` 运行起来。这是验证容器化环境和开发流程的第一步。
|
||||
|
||||
**任务**:
|
||||
1. 在项目根目录创建 `docker-compose.yml` 文件,定义 `frontend`, `backend`, `postgres-db` 三个服务。
|
||||
2. 分别为 `frontend` 和 `backend` 目录创建 `Dockerfile`。
|
||||
3. 在项目根目录创建 `Tiltfile`,并配置其加载 `docker-compose.yml`。
|
||||
4. 调整配置文件(如 `NEXT_PUBLIC_BACKEND_URL` 和 `DATABASE_URL` 环境变量),使其适应Docker内部网络。
|
||||
5. **验证**: 运行 `tilt up`,整个应用能够像在本地一样正常启动和访问。
|
||||
|
||||
---
|
||||
|
||||
### 阶段 1: 拆分配置服务 (`config-service`)
|
||||
|
||||
**目标**: 将配置管理逻辑从主后端中剥离,创建第一个真正的微服务。这是一个理想的起点,因为它依赖项少,风险低。
|
||||
|
||||
**任务**:
|
||||
1. 创建新目录 `services/config-service`。
|
||||
2. 在该目录中初始化一个新的、轻量级的FastAPI应用。
|
||||
3. 将原 `backend` 中所有读取 `config/` 目录文件的逻辑(如 `ConfigManager`) 迁移至 `config-service`。
|
||||
4. 在 `config-service` 中暴露API端点,例如 `GET /api/v1/system`, `GET /api/v1/analysis-modules`。
|
||||
5. 在 `docker-compose.yml` 中新增 `config-service` 的定义。
|
||||
6. 修改原 `backend` 应用,移除本地文件读取逻辑,改为通过HTTP请求从 `config-service` 获取配置。
|
||||
|
||||
---
|
||||
|
||||
### 阶段 2: 拆分数据聚合服务 (`data-aggregator`)
|
||||
|
||||
**目标**: 将所有与外部数据源的交互逻辑隔离出来。
|
||||
|
||||
**任务**:
|
||||
1. 创建新目录 `services/data-aggregator`。
|
||||
2. 将原 `backend/app/data_providers` 目录及相关的数据获取和处理逻辑整体迁移到新服务中。
|
||||
3. 为新服务定义清晰的API,例如 `GET /api/v1/financials/{symbol}`。
|
||||
4. 在 `docker-compose.yml` 中新增 `data-aggregator` 服务的定义。
|
||||
5. 修改原 `backend` 应用,将调用本地数据模块改为通过HTTP请求调用 `data-aggregator` 服务。
|
||||
|
||||
---
|
||||
|
||||
### 阶段 3: 拆分AI分析服务 (`analysis-service`)
|
||||
|
||||
**目标**: 隔离计算密集型且可能需要特殊硬件资源的AI调用逻辑。
|
||||
|
||||
**任务**:
|
||||
1. 创建新目录 `services/analysis-service`。
|
||||
2. 将原 `backend/app/services/analysis_client.py` 及相关的Gemini API调用逻辑迁移到新服务中。
|
||||
3. 定义API,例如 `POST /api/v1/analyze`,接收上下文数据和prompt,返回分析结果。
|
||||
4. 在 `docker-compose.yml` 中新增 `analysis-service` 的定义。
|
||||
5. 修改原 `backend` 应用,将直接调用SDK改为通过HTTP请求调用 `analysis-service`。
|
||||
|
||||
---
|
||||
|
||||
### 阶段 4: 引入API网关 (`api-gateway`) 并重塑编排器
|
||||
|
||||
**目标**: 建立统一的外部入口,并正式将原 `backend` 的角色明确为 `report-orchestrator`。
|
||||
|
||||
**任务**:
|
||||
1. 创建新目录 `services/api-gateway`,并初始化一个FastAPI应用。
|
||||
2. 在 `api-gateway` 中配置路由规则,将来自前端的请求(如 `/api/config/*`, `/api/financials/*`)代理到对应的内部微服务 (`config-service`, `report-orchestrator` 等)。
|
||||
3. 更新 `docker-compose.yml`,将前端端口暴露给主机,而其他后端服务仅在内部网络可达。
|
||||
4. 修改 `frontend` 的 `NEXT_PUBLIC_BACKEND_URL` 指向 `api-gateway`。
|
||||
5. 此时,原 `backend` 的代码已经精简,主要剩下编排逻辑。我们可以考虑将其目录重命名为 `services/report-orchestrator`,以准确反映其职责。
|
||||
|
||||
## 4. 最终项目目录结构(设想)
|
||||
|
||||
重构完成后,项目目录结构将演变为:
|
||||
|
||||
```
|
||||
/home/lv/nvm/works/Fundamental_Analysis/
|
||||
├── docker-compose.yml
|
||||
├── Tiltfile
|
||||
├── README.md
|
||||
├── .gitignore
|
||||
├── services/
|
||||
│ ├── frontend/
|
||||
│ │ └── Dockerfile
|
||||
│ ├── api-gateway/
|
||||
│ │ ├── app/
|
||||
│ │ └── Dockerfile
|
||||
│ ├── config-service/
|
||||
│ │ ├── app/
|
||||
│ │ └── Dockerfile
|
||||
│ ├── data-aggregator/
|
||||
│ │ ├── app/
|
||||
│ │ └── Dockerfile
|
||||
│ ├── analysis-service/
|
||||
│ │ ├── app/
|
||||
│ │ └── Dockerfile
|
||||
│ └── report-orchestrator/ # 由原 backend 演变而来
|
||||
│ ├── app/
|
||||
│ └── Dockerfile
|
||||
├── deployment/
|
||||
│ └── pm2.config.js
|
||||
├── scripts/
|
||||
│ ├── dev.py
|
||||
│ └── package.json # 原根目录的 package.json
|
||||
├── config/ # 静态配置文件,由 config-service 读取
|
||||
└── docs/
|
||||
└── microservice_refactoring_plan.md
|
||||
```
|
||||
|
||||
## 5. 结论
|
||||
|
||||
本计划提供了一个从单体到微服务的清晰、可行的演进路径。通过分阶段、增量式的重构,我们可以平稳地完成架构升级,同时确保在每个阶段结束后,系统都处于可工作、可验证的状态。
|
||||
|
||||
请您审阅此计划。如有任何疑问或建议,我们可以随时讨论和调整。
|
||||
22
frontend/Dockerfile
Normal file
22
frontend/Dockerfile
Normal file
@ -0,0 +1,22 @@
|
||||
# syntax=docker/dockerfile:1.6
|
||||
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
ENV NODE_ENV=development \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
CI=false
|
||||
|
||||
WORKDIR /workspace/frontend
|
||||
|
||||
# 仅复制依赖清单,最大化利用缓存
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
|
||||
# 使用 npm ci(若失败则回退 npm install,避免镜像构建被锁文件问题卡住)
|
||||
RUN npm ci || npm install
|
||||
|
||||
# 运行时通过挂载卷提供源码
|
||||
RUN mkdir -p /workspace/frontend
|
||||
|
||||
# 缺省入口由 docker-compose 提供
|
||||
|
||||
|
||||
@ -13,18 +13,6 @@ const nextConfig = {
|
||||
experimental: {
|
||||
proxyTimeout: 300000, // 300 seconds (5 minutes)
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/api/:path*",
|
||||
destination: "http://127.0.0.1:8000/api/:path*",
|
||||
},
|
||||
{
|
||||
source: "/health",
|
||||
destination: "http://127.0.0.1:8000/health",
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@ -1,14 +1,20 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
const BACKEND_BASE = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://127.0.0.1:8000/api';
|
||||
const BACKEND_BASE = process.env.NEXT_PUBLIC_BACKEND_URL;
|
||||
|
||||
export async function GET() {
|
||||
if (!BACKEND_BASE) {
|
||||
return new Response('NEXT_PUBLIC_BACKEND_URL 未配置', { status: 500 });
|
||||
}
|
||||
const resp = await fetch(`${BACKEND_BASE}/config`);
|
||||
const text = await resp.text();
|
||||
return new Response(text, { status: resp.status, headers: { 'Content-Type': resp.headers.get('Content-Type') || 'application/json' } });
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest) {
|
||||
if (!BACKEND_BASE) {
|
||||
return new Response('NEXT_PUBLIC_BACKEND_URL 未配置', { status: 500 });
|
||||
}
|
||||
const body = await req.text();
|
||||
const resp = await fetch(`${BACKEND_BASE}/config`, {
|
||||
method: 'PUT',
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
const BACKEND_BASE = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://127.0.0.1:8000/api';
|
||||
const BACKEND_BASE = process.env.NEXT_PUBLIC_BACKEND_URL;
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
if (!BACKEND_BASE) {
|
||||
return new Response('NEXT_PUBLIC_BACKEND_URL 未配置', { status: 500 });
|
||||
}
|
||||
const body = await req.text();
|
||||
const resp = await fetch(`${BACKEND_BASE}/config/test`, {
|
||||
method: 'POST',
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
const BACKEND_BASE = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://127.0.0.1:8000/api';
|
||||
const BACKEND_BASE = process.env.NEXT_PUBLIC_BACKEND_URL;
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
context: { params: Promise<{ slug: string[] }> }
|
||||
) {
|
||||
if (!BACKEND_BASE) {
|
||||
return new Response('NEXT_PUBLIC_BACKEND_URL 未配置', { status: 500 });
|
||||
}
|
||||
const url = new URL(req.url);
|
||||
const { slug } = await context.params;
|
||||
const path = slug.join('/');
|
||||
|
||||
83
package-lock.json
generated
83
package-lock.json
generated
@ -1,83 +0,0 @@
|
||||
{
|
||||
"name": "Fundamental_Analysis",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"swr": "^2.3.6",
|
||||
"zustand": "^5.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/swr": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz",
|
||||
"integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
|
||||
"integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"swr": "^2.3.6",
|
||||
"zustand": "^5.0.8"
|
||||
}
|
||||
}
|
||||
5
dev.py → scripts/dev.py
Executable file → Normal file
5
dev.py → scripts/dev.py
Executable file → Normal file
@ -108,7 +108,8 @@ def main():
|
||||
parser.add_argument("--backend-app", default=os.getenv("BACKEND_APP", "main:app"), help="Uvicorn app path, e.g. main:app")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = Path(__file__).resolve().parent
|
||||
# scripts/dev.py -> 仓库根目录
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
backend_dir = repo_root / "backend"
|
||||
frontend_dir = repo_root / "frontend"
|
||||
|
||||
@ -204,3 +205,5 @@ def main():
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
21
services/config-service/Dockerfile
Normal file
21
services/config-service/Dockerfile
Normal file
@ -0,0 +1,21 @@
|
||||
# syntax=docker/dockerfile:1.6
|
||||
|
||||
FROM python:3.11-slim AS base
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=1 \
|
||||
PROJECT_ROOT=/workspace
|
||||
|
||||
WORKDIR /workspace/services/config-service
|
||||
|
||||
COPY services/config-service/requirements.txt ./requirements.txt
|
||||
RUN pip install --upgrade pip && \
|
||||
pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 运行时通过挂载卷提供配置与源码
|
||||
RUN mkdir -p /workspace/services/config-service
|
||||
|
||||
# 缺省入口由 docker-compose 提供
|
||||
|
||||
|
||||
64
services/config-service/app/main.py
Normal file
64
services/config-service/app/main.py
Normal file
@ -0,0 +1,64 @@
|
||||
"""
|
||||
Config Service - provides read-only access to static configuration files.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any, Dict
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
APP_NAME = "config-service"
|
||||
API_V1 = "/api/v1"
|
||||
# 在容器内挂载了项目根到 /workspace
|
||||
PROJECT_ROOT = os.environ.get("PROJECT_ROOT", "/workspace")
|
||||
CONFIG_DIR = os.path.join(PROJECT_ROOT, "config")
|
||||
SYSTEM_CONFIG_PATH = os.path.join(CONFIG_DIR, "config.json")
|
||||
ANALYSIS_CONFIG_PATH = os.path.join(CONFIG_DIR, "analysis-config.json")
|
||||
|
||||
app = FastAPI(title=APP_NAME, version="0.1.0")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
def _read_json_file(path: str) -> Dict[str, Any]:
|
||||
if not os.path.exists(path):
|
||||
raise HTTPException(status_code=404, detail=f"配置文件不存在: {os.path.basename(path)}")
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
raise HTTPException(status_code=500, detail=f"配置文件格式错误: {e}") from e
|
||||
except OSError as e:
|
||||
raise HTTPException(status_code=500, detail=f"读取配置文件失败: {e}") from e
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root() -> Dict[str, Any]:
|
||||
return {"status": "ok", "name": APP_NAME}
|
||||
|
||||
|
||||
@app.get(f"{API_V1}/system")
|
||||
async def get_system_config() -> Dict[str, Any]:
|
||||
"""
|
||||
返回系统基础配置(纯文件内容,不包含数据库覆盖)。
|
||||
"""
|
||||
return _read_json_file(SYSTEM_CONFIG_PATH)
|
||||
|
||||
|
||||
@app.get(f"{API_V1}/analysis-modules")
|
||||
async def get_analysis_modules() -> Dict[str, Any]:
|
||||
"""
|
||||
返回分析模块配置(原样透传)。
|
||||
"""
|
||||
return _read_json_file(ANALYSIS_CONFIG_PATH)
|
||||
|
||||
|
||||
3
services/config-service/requirements.txt
Normal file
3
services/config-service/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
fastapi==0.115.0
|
||||
uvicorn[standard]==0.30.6
|
||||
|
||||
Loading…
Reference in New Issue
Block a user