# ODMS Connect 用户手册

> 版本：v2.0 · 日期：2026-05-24  
> 面向角色：IT 负责人、数据管理员

---

## 1. 产品定位

Connect 将企业内部关系数据库（PostgreSQL / MySQL / SQL Server）暴露为标准 OData v4 接口，让 Studio、Insight 及外部系统通过统一协议访问数据，**不搬迁数据，不改动原系统**。

**Connect 能做什么：**
- 连接关系数据库，自动读取表结构
- 选择表/视图定义 Entity，控制暴露哪些字段
- 将多个 Entity 发布为 OData 服务（Runtime 端点）
- 通过 Client Credential 管控哪些系统可以访问哪些 Entity

**Connect 不做什么：**
- 不做 SaaS / Excel / REST API 接入（用 CData 等工具转 OData 后接 Studio）
- 不做字段级/行级权限（由 Studio 承担）
- 不做写操作审批（走 Insight Action）

---

## 2. 核心概念

| 概念 | 说明 |
|------|------|
| **DataSource** | 一个数据库连接配置（host / port / database / 账号密码）|
| **Entity** | 对应数据库中的一张表或视图，定义对外暴露哪些字段 |
| **Service** | 将若干 Entity 打包发布的 OData 端点（如 `/AuroraTradingService/`）|
| **Runtime** | 无状态的 OData 服务进程，向 Backend 注册后自动同步配置，10 秒心跳 |
| **Client** | 访问 Runtime 的调用方身份，持有 client_id + client_secret，绑定 Entity 白名单和 HTTP 方法权限 |

---

## 3. 快速开始（五步完成接入）

```
步骤 1：添加数据源   → 填写数据库连接信息
步骤 2：创建服务     → 创建 OData 服务，选择 Entity
步骤 3：发布服务     → Runtime 自动加载，端点生效
步骤 4：创建 Client  → 分配访问权限给调用方
步骤 5：获取 Token   → POST /oauth/token，携带 Bearer JWT 调用 OData
```

---

## 4. 操作指南

### 4.1 启动服务（首次使用）

启动 Connect 三个进程：

```powershell
# 终端 1 — Backend（配置管理，端口 8000）
cd odms-connect
uv run uvicorn backend.main:app --port 8000 --reload

# 终端 2 — Runtime（OData 服务，端口 8100）
cd odms-connect/runtime
# 设置必要环境变量
$env:CONNECT_BACKEND_URL    = "http://127.0.0.1:8000"
$env:CONNECT_RUNTIME_ID     = "runtime-local"
$env:CONNECT_RUNTIME_NAME   = "Local Runtime"
$env:CONNECT_RUNTIME_BASE_URL = "http://127.0.0.1:8100"
uv run uvicorn main:app --port 8100 --reload

# 终端 3 — Frontend（管理界面，端口 3000）
cd odms-connect/frontend
npm run dev
```

验证启动：

```bash
curl http://localhost:8000/health   # → {"status":"ok","service":"connect-backend"}
curl http://localhost:8100/health   # → {"status":"ok","service":"connect-runtime"}
```

首次使用需初始化管理员账号（执行一次）：

```bash
cd odms-connect/backend
uv run python scripts/seed.py
# 初始账号：admin@example.com / admin123
```

### 4.2 登录管理界面

打开浏览器访问 `http://localhost:3000`，跳转到登录页。

| 字段 | 初始值 |
|------|--------|
| 邮箱 | `admin@example.com` |
| 密码 | `admin123` |

登录后进入 Dashboard，顶部显示数据源数、服务数、Runtime 数统计。

### 4.3 添加数据源

1. 左侧导航 → **数据源** → 右上角「**+ 新建数据源**」
2. 填写：
   - **名称**：内部标识（如 `prod-pgsql`）
   - **类型**：postgres / sqlserver / mysql
   - **连接 URL**：`postgresql://用户名:密码@host:5432/数据库名`
   - **默认 Schema**（可选）：填写后自省优先展示该 schema
3. 「测试连接」→ 通过后「创建」

> 数据库账号建议使用只读账号；密码加密存储，不可明文查看。

### 4.4 自省数据库结构

在数据源列表点击「**自省**」，系统读取：
- 所有 Schema
- 每个 Schema 下的表和视图列表
- 每张表的字段（名称、DB 类型、EDM 类型、是否主键、是否可空）

自省结果用于后续创建 Entity 时参考字段类型。

### 4.5 创建服务并定义 Entity

1. 左侧导航 → **服务** → 「**+ 新建服务**」
2. 填写：
   - **服务名**：OData 路径前缀（如 `AuroraTradingService`），建议英文大驼峰
   - **绑定数据源**：选择第 4.3 步创建的数据源
   - **绑定 Runtime**：选择已注册的 Runtime（默认 `runtime-local`）
3. 点击「**添加 Entity**」，逐个配置：
   - **Entity 名称**：OData EntitySet 名（如 `Customers`）
   - **源 Schema**：数据库 Schema 名
   - **源表**：表名或视图名
   - **主键字段**：唯一标识字段（如 `customer_id`）
   - **字段列表**：每个字段配置 EDM 类型、是否可空、是否只读

**字段只读说明：** `is_readonly: true` 的字段在 POST/PATCH 时调用方不可传入，常用于主键、创建时间等系统字段。

### 4.6 发布服务

服务创建后默认为草稿状态。在服务列表点击「**发布**」：
- Runtime 在下次心跳（最多 10 秒）后自动加载配置
- 状态变为「已发布」后端点生效

**验证发布成功：**
```bash
# $metadata 无需认证，可直接访问
curl http://localhost:8100/AuroraTradingService/$metadata

# 服务文档
curl http://localhost:8100/AuroraTradingService/
```

### 4.7 创建 Client（访问控制）

> Client 管理在 **Connect Dashboard → 左侧「客户端」**，由 IT 负责人或数据管理员操作。

1. 左侧导航 → **客户端** → 「**+ 新建客户端**」
2. 填写：
   - **Client 名称**：标识调用方（如 `insight-app`、`erp-sync`）
   - **绑定服务**：选择要授权访问的 Service
   - **允许 Entity**：勾选此 Client 可访问的 Entity（零权限默认，需显式开放）
   - **允许 HTTP 方法**：`GET`（查询）/ `POST`（新增）/ `PATCH`（更新）/ `DELETE`（删除）
   - **有效期**（可选）：默认永久
3. 「创建」后，系统显示：
   - `client_id`：`cid_xxxxxxxxxxxx`
   - `client_secret`：`xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`（**仅显示一次，请立即复制保存**）

> Secret 不可找回。忘记后需吊销原 Client，重新创建。

### 4.8 获取 OAuth Token

调用方拿到 client_id + client_secret 后，向 Connect Backend 获取访问令牌：

```bash
curl -X POST http://localhost:8000/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&client_id=cid_xxx&client_secret=xxxxxxxx"
```

**响应：**
```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "expires_in": 3600
}
```

Token 有效期 1 小时，到期后重新请求即可。

### 4.9 调用 OData 接口

携带 Bearer Token 调用 Runtime OData 端点：

```bash
# 查询客户列表（前 20 条，按余额降序）
curl "http://localhost:8100/AuroraTradingService/Customers?$top=20&$orderby=balance desc" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# 按条件过滤
curl "http://localhost:8100/AuroraTradingService/Customers?$filter=risk_level eq 'high'" \
  -H "Authorization: Bearer <token>"

# 带总数分页
curl "http://localhost:8100/AuroraTradingService/Customers?$top=20&$skip=0&$count=true" \
  -H "Authorization: Bearer <token>"

# 只返回指定字段
curl "http://localhost:8100/AuroraTradingService/Customers?$select=customer_id,name,balance" \
  -H "Authorization: Bearer <token>"

# 单条查询
curl "http://localhost:8100/AuroraTradingService/Customers('C001')" \
  -H "Authorization: Bearer <token>"
```

**`$metadata` 无需 Token，可公开访问（供上层系统发现 Schema）：**
```bash
curl http://localhost:8100/AuroraTradingService/$metadata
```

**常用 OData 查询参数：**

| 参数 | 说明 | 示例 |
|------|------|------|
| `$top` | 返回前 N 条 | `$top=20` |
| `$skip` | 跳过前 N 条 | `$skip=20` |
| `$count=true` | 返回总条数 | `$count=true` |
| `$filter` | 条件过滤 | `$filter=status eq 'active'` |
| `$select` | 指定字段 | `$select=id,name` |
| `$orderby` | 排序 | `$orderby=balance desc` |

**`$filter` 运算符：**
- `eq / ne / gt / ge / lt / le`：等于 / 不等于 / 大于 / 大于等于 / 小于 / 小于等于
- `and / or / not`：逻辑运算
- `contains(field,'val')`：模糊匹配

### 4.10 查看审计日志

**Connect Dashboard → 左侧「审计」**，显示两类日志：

| 日志类型 | 内容 | 用途 |
|---------|------|------|
| 配置变更 | 谁创建/修改/删除了数据源、服务、Client | IT 配置追踪 |
| 访问日志 | 哪个 Client、访问了哪个 Entity、HTTP 方法、结果 | 数据访问审计 |

可按日志类型过滤查看，支持时间倒序排列（最新在前）。

---

## 5. 安全说明

| 场景 | 处理方式 |
|------|---------|
| Client Secret 泄露 | 在 Connect 客户端管理中立即吊销，不影响其他 Client |
| 某集成系统下线 | 吊销对应 Client，实时生效 |
| 排查"谁查了某张表" | 审计日志 → 访问日志，按 Entity 过滤 |
| Secret 忘记了 | 无法找回；吊销旧 Client，创建新 Client |
| 新建 Client 默认权限 | 零权限；需显式选择 Entity 和 HTTP 方法 |
| Token 过期 | 重新请求 `/oauth/token` 获取新 Token |

---

## 6. 常见问题

**Q：发布服务后访问 OData 返回 404？**  
A：等待 Runtime 心跳（最多 10 秒）。确认服务状态为「已发布」，Runtime 状态为「在线」。

**Q：调用方收到 401 Unauthorized？**  
A：检查 Authorization 头格式：`Bearer <token>`。检查 Token 是否已过期（1 小时有效）。`$metadata` 路径不需要 Token。

**Q：调用方收到 403 Entity not allowed？**  
A：该 Client 的 allowed_entities 不包含请求的 Entity。在客户端管理界面检查权限配置。

**Q：调用方收到 403 Method not allowed？**  
A：该 Client 的 allowed_methods 不包含请求的 HTTP 方法（如 POST）。在权限配置中添加对应方法。

**Q：Runtime 离线了怎么办？**  
A：Runtime 无状态，重启后自动向 Backend 重新注册并拉取配置，端点自动恢复。

**Q：同一个 Entity 能加入多个服务吗？**  
A：可以。不同 Service 可配置不同 Client 权限，同一 Entity 在不同服务中的访问是独立控制的。

---

## 7. 晨曦贸易案例：完整搭建过程

> 本节以晨曦贸易为例，演示从零开始接入一个完整业务数据库的全流程。

### 7.1 背景

晨曦贸易有一个 PostgreSQL 业务数据库（`odms_demo_aurora_trading`），包含客户、商品、销售订单、发票、回款、催收任务等核心业务表，以及一个「逾期发票」视图。

**目标：** 通过 Connect 将这 8 个业务对象暴露为 OData 服务，供 ODMS Insight 等上层应用直接消费。

**数据库结构：**

| 表/视图名 | 类型 | 业务含义 |
|----------|------|---------|
| `customers` | 表 | 客户主档 |
| `products` | 表 | 商品目录 |
| `sales_orders` | 表 | 销售订单主表 |
| `sales_order_lines` | 表 | 订单明细 |
| `invoices` | 表 | 发票 |
| `payments` | 表 | 回款记录 |
| `collection_tasks` | 表 | 催收任务 |
| `invoice_overdue` | **视图** | 逾期发票（含逾期天数计算列）|

### 7.2 前提

1. PostgreSQL 已运行，`odms_demo_aurora_trading` 库已初始化（含 `aurora_trading` schema）
2. Connect 三个进程已启动（参见第 4.1 节）
3. 已用 `admin@example.com` / `admin123` 登录管理界面

### 7.3 Step 1 — 创建数据源

**Dashboard → 数据源 → + 新建数据源**

| 字段 | 填写值 |
|------|--------|
| 名称 | `晨曦贸易 DB` |
| 类型 | `postgres` |
| 连接 URL | `postgresql://postgres:800309@localhost:5432/odms_demo_aurora_trading` |
| 默认 Schema | `aurora_trading` |

创建成功后，点击「自省」，应看到 `aurora_trading` schema 下 7 张表和 1 个视图。

### 7.4 Step 2 — 创建 AuroraTradingService

**Dashboard → 服务 → + 新建服务**

基本信息：

| 字段 | 填写值 |
|------|--------|
| 服务名 | `AuroraTradingService` |
| 绑定数据源 | `晨曦贸易 DB` |
| 绑定 Runtime | `runtime-local` |

**逐一添加 8 个 Entity：**

---

**Entity 1 — Customers（客户）**

| 属性 | 值 |
|------|----|
| Entity 名称 | `Customers` |
| 源 Schema | `aurora_trading` |
| 源表 | `customers` |
| 主键字段 | `customer_id` |

字段：

| 字段名 | EDM 类型 | 只读 |
|--------|---------|------|
| customer_id | Edm.String | ✓ |
| name | Edm.String | ✓ |
| contact_person | Edm.String | ✓ |
| phone | Edm.String | ✓ |
| credit_limit | Edm.Decimal | ✓ |
| balance | Edm.Decimal | ✓ |
| risk_level | Edm.String | ✓ |
| blocked | Edm.Boolean | ✓ |
| account_manager | Edm.String | ✓ |
| created_at | Edm.DateTimeOffset | ✓ |

---

**Entity 2 — Products（商品）**

| 属性 | 值 |
|------|----|
| Entity 名称 | `Products` |
| 源 Schema | `aurora_trading` |
| 源表 | `products` |
| 主键字段 | `product_id` |

字段：

| 字段名 | EDM 类型 | 只读 |
|--------|---------|------|
| product_id | Edm.String | ✓ |
| name | Edm.String | ✓ |
| category | Edm.String | ✓ |
| unit_price | Edm.Decimal | ✓ |
| unit | Edm.String | ✓ |
| stock | Edm.Int32 | ✓ |
| reorder_point | Edm.Int32 | ✓ |

---

**Entity 3 — SalesOrders（销售订单）**

| 属性 | 值 |
|------|----|
| Entity 名称 | `SalesOrders` |
| 源 Schema | `aurora_trading` |
| 源表 | `sales_orders` |
| 主键字段 | `order_id` |

字段：

| 字段名 | EDM 类型 | 只读 |
|--------|---------|------|
| order_id | Edm.String | ✓ |
| customer_id | Edm.String | ✓ |
| order_date | Edm.Date | ✓ |
| total_amount | Edm.Decimal | ✓ |
| status | Edm.String | ✓ |
| created_by | Edm.String | ✓ |

---

**Entity 4 — SalesOrderLines（订单明细）**

| 属性 | 值 |
|------|----|
| Entity 名称 | `SalesOrderLines` |
| 源 Schema | `aurora_trading` |
| 源表 | `sales_order_lines` |
| 主键字段 | `line_id` |

字段：

| 字段名 | EDM 类型 | 只读 |
|--------|---------|------|
| line_id | Edm.Int32 | ✓ |
| order_id | Edm.String | ✓ |
| product_id | Edm.String | ✓ |
| quantity | Edm.Int32 | ✓ |
| unit_price | Edm.Decimal | ✓ |
| line_amount | Edm.Decimal | ✓ |
| discount | Edm.Decimal | ✓ |

---

**Entity 5 — Invoices（发票）**

| 属性 | 值 |
|------|----|
| Entity 名称 | `Invoices` |
| 源 Schema | `aurora_trading` |
| 源表 | `invoices` |
| 主键字段 | `invoice_id` |

字段：

| 字段名 | EDM 类型 | 只读 |
|--------|---------|------|
| invoice_id | Edm.String | ✓ |
| customer_id | Edm.String | ✓ |
| order_id | Edm.String | ✓ |
| invoice_date | Edm.Date | ✓ |
| due_date | Edm.Date | ✓ |
| amount | Edm.Decimal | ✓ |
| paid | Edm.Boolean | ✓ |
| paid_date | Edm.Date | ✓ |

---

**Entity 6 — InvoiceOverdue（逾期发票视图）**

> 这是一个**视图**，source_table 填视图名即可，与普通表操作完全相同。

| 属性 | 值 |
|------|----|
| Entity 名称 | `InvoiceOverdue` |
| 源 Schema | `aurora_trading` |
| 源表 | `invoice_overdue`（视图）|
| 主键字段 | `invoice_id` |

字段：

| 字段名 | EDM 类型 | 只读 |
|--------|---------|------|
| invoice_id | Edm.String | ✓ |
| customer_id | Edm.String | ✓ |
| order_id | Edm.String | ✓ |
| invoice_date | Edm.Date | ✓ |
| due_date | Edm.Date | ✓ |
| amount | Edm.Decimal | ✓ |
| paid | Edm.Boolean | ✓ |
| paid_date | Edm.Date | ✓ |
| overdue_days | Edm.Int32 | ✓（计算列）|

---

**Entity 7 — Payments（回款）**

| 属性 | 值 |
|------|----|
| Entity 名称 | `Payments` |
| 源 Schema | `aurora_trading` |
| 源表 | `payments` |
| 主键字段 | `payment_id` |

字段：

| 字段名 | EDM 类型 | 只读 |
|--------|---------|------|
| payment_id | Edm.String | ✓ |
| customer_id | Edm.String | ✓ |
| invoice_id | Edm.String | ✓ |
| payment_date | Edm.Date | ✓ |
| amount | Edm.Decimal | ✓ |
| method | Edm.String | ✓ |
| note | Edm.String | ✓ |

---

**Entity 8 — CollectionTasks（催收任务）**

| 属性 | 值 |
|------|----|
| Entity 名称 | `CollectionTasks` |
| 源 Schema | `aurora_trading` |
| 源表 | `collection_tasks` |
| 主键字段 | `task_id` |

字段：

| 字段名 | EDM 类型 | 只读 |
|--------|---------|------|
| task_id | Edm.Int32 | ✓ |
| customer_id | Edm.String | ✓ |
| invoice_id | Edm.String | ✓ |
| assignee_id | Edm.String | ✓ |
| status | Edm.String | ✓ |
| priority | Edm.String | ✓ |
| due_date | Edm.Date | ✓ |
| note | Edm.String | ✓ |
| created_at | Edm.DateTimeOffset | ✓ |
| resolved_at | Edm.DateTimeOffset | ✓ |

---

### 7.5 Step 3 — 发布服务

服务列表找到「AuroraTradingService」→「**发布**」。

等待约 10 秒（Runtime 心跳），状态变为「已发布」。

验证：
```bash
# 元数据（无需 Token）
curl http://localhost:8100/AuroraTradingService/$metadata

# 应看到 8 个 EntitySet 定义
```

### 7.6 Step 4 — 创建 Client

**Dashboard → 客户端 → + 新建客户端**

为 Insight 应用创建访问凭证：

| 字段 | 填写值 |
|------|--------|
| Client 名称 | `insight-aurora_trading` |
| 绑定服务 | `AuroraTradingService` |
| 允许 Entity | 全部 8 个（Customers / Products / SalesOrders / SalesOrderLines / Invoices / InvoiceOverdue / Payments / CollectionTasks）|
| 允许方法 | `GET`（只读场景）|
| 有效期 | 不填（永久）|

创建后，**立即复制并保存** Client ID 和 Client Secret（格式如下）：

```
client_id:     cid_xxxxxxxxxxxx
client_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

### 7.7 Step 5 — 获取 Token 并验证访问

**获取访问 Token：**

```bash
curl -X POST http://localhost:8000/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&client_id=cid_xxxxxxxxxxxx&client_secret=xxxxxxxx"
```

响应：
```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "expires_in": 3600
}
```

**验证各 Entity 可访问：**

```bash
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# 客户列表（按信用余额降序）
curl "http://localhost:8100/AuroraTradingService/Customers?$orderby=balance desc&$top=5" \
  -H "Authorization: Bearer $TOKEN"

# 高风险客户
curl "http://localhost:8100/AuroraTradingService/Customers?$filter=risk_level eq 'high'" \
  -H "Authorization: Bearer $TOKEN"

# 逾期发票（视图）
curl "http://localhost:8100/AuroraTradingService/InvoiceOverdue?$orderby=overdue_days desc" \
  -H "Authorization: Bearer $TOKEN"

# 待处理催收任务
curl "http://localhost:8100/AuroraTradingService/CollectionTasks?$filter=status eq 'open'" \
  -H "Authorization: Bearer $TOKEN"

# 按主键查单条（字符串主键需加引号）
curl "http://localhost:8100/AuroraTradingService/Customers('C001')" \
  -H "Authorization: Bearer $TOKEN"
```

### 7.8 Step 6 — 查看审计记录

**Dashboard → 审计**

- **配置变更日志**：应看到「创建数据源」「创建服务」「发布服务」「创建 Client」各一条，actor 为 `admin@example.com`
- **访问日志**：每次用 Token 调用 OData 后，约 1-2 秒内出现对应的访问记录，包含 client_id、Entity 名、HTTP 方法和状态码

### 7.9 完整链路示意

```
odms_demo_aurora_trading（PostgreSQL）
  schema: aurora_trading
  tables: customers / products / sales_orders / ...
  view:   invoice_overdue
        │
        │ asyncpg 实时查询
        ▼
┌─────────────────────────────┐
│  ODMS Connect Runtime       │
│  :8100                      │
│                             │
│  AuroraTradingService/          │
│    Customers                │
│    Products                 │
│    SalesOrders              │
│    SalesOrderLines          │
│    Invoices                 │
│    InvoiceOverdue  ← 视图   │
│    Payments                 │
│    CollectionTasks          │
└──────────────┬──────────────┘
               │  OData v4（HTTP + Bearer JWT）
               ▼
       ODMS Insight / 外部系统
  GET /AuroraTradingService/Customers?$filter=risk_level eq 'high'
  GET /AuroraTradingService/InvoiceOverdue?$orderby=overdue_days desc
  GET /AuroraTradingService/CollectionTasks?$filter=status eq 'open'
```

至此，晨曦贸易的 8 个核心业务对象均已通过标准 OData 接口对外暴露，Insight 等上层模块可直接消费，无需了解底层数据库结构。

