# 科研开放数据接口 — 技术方案

> 依据：同目录 `科研开放数据接口功能需求.md`。对外只读开放 API；**无**前端页面、**无**新表。

---

## 1. 技术架构

| 项 | 说明 |
| --- | --- |
| **后端** | RuoYi **v3.9.2**（**springboot2** 分支）：JDK 8、Spring MVC、MyBatis |
| **数据库** | 只读查询既有表 `biz_pasture`、`biz_yak_asset` |
| **鉴权** | 独立 JWT + Servlet Filter；`@Anonymous` 绕过若依登录，Filter 校验 Bearer Token |
| **响应** | `OpenApiResponse<T>` + `OpenApiPageResult<T>`（与第三方 farming OpenAPI 同结构） |

**代码包**：`baqing-admin` → `com.ruoyi.web.modules.industryservice.research`

| 类 | 职责 |
| --- | --- |
| `ResearchOpenApiController` | HTTP 入口 |
| `ResearchOpenApiServiceImpl` | 分页查询 + 脱敏 VO 转换 |
| `ResearchOpenApiTokenService` | 签发/校验 JWT |
| `ResearchOpenApiAuthFilter` | 除 `/auth/token` 外校验 Token |
| `ResearchOpenApiMasking` / `ResearchOpenApiSupport` | 脱敏与响应组装 |

---

## 2. 配置

`application.yml` → `research-open-api`：

| 键 | 说明 |
| --- | --- |
| `enabled` | 是否启用；`false` 时返回 503 |
| `app-key` | 申请 Token 用（全局唯一） |
| `app-secret` | 申请 Token 用 |
| `token-secret` | JWT 签名密钥（**仅服务端**，勿下发） |
| `token-expire-minutes` | Token 有效期，默认 120 |
| `max-page-size` | 单页上限，默认 200 |

**示例：**

```yaml
research-open-api:
  enabled: true
  app-key: research-open-demo-key
  app-secret: research-open-demo-secret-change-me
  token-secret: research-open-api-change-me-in-production
  token-expire-minutes: 120
  max-page-size: 200
```

---

## 3. 接口设计

**Base Path**：`/open-api/v1/research`

### 3.1 申请 Token

```
POST /open-api/v1/research/auth/token
Content-Type: application/json

{"appKey":"research-open-demo-key","appSecret":"research-open-demo-secret-change-me"}
```

**成功响应 `data`：**

```json
{
  "accessToken": "eyJhbGciOiJIUzUxMiJ9...",
  "tokenType": "Bearer",
  "expiresIn": 7200
}
```

JWT Claims：`type=research_open_api`；**不含**机构 ID。

### 3.2 牧场列表

```
GET /open-api/v1/research/pastures?pageNum=1&pageSize=20&keyword=
Authorization: Bearer <accessToken>
```

**`records[]` 元素（ResearchPastureVo）：**

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `pastureId` | long | 本地牧场 ID |
| `pastureName` | string | 牧场名称 |
| `farmType` | string | 类型 |
| `county` / `town` | string | 区县、乡镇 |
| `regionSummary` | string | 区县+乡镇摘要 |
| `latitude` / `longitude` | number | 约百米精度 |
| `floorArea` / `scaleBreeding` / `breedSpecies` | string | 规模类 |
| `introduction` | string | 简介 |
| `personInCharge` | string | 脱敏负责人 |
| `contactPhone` | string | 脱敏手机 |

### 3.3 牦牛资产档案列表

```
GET /open-api/v1/research/yak-assets?pageNum=1&pageSize=20&assetStatus=1&gender=母&pastureId=
Authorization: Bearer <accessToken>
```

**`records[]` 元素（ResearchYakAssetVo）：**

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `subjectCode` | string | 科研主体码，如 `YS00000088` |
| `pastureName` / `enclosureName` | string | 牧场、圈舍名称 |
| `gender` / `cattleVariety` | string | 性别、品种 |
| `ageMonths` | int | 月龄 |
| `birthYear` / `birthMonth` | int | 出生年月 |
| `assetStatus` | int | 1～4 |
| `assetStatusLabel` | string | 正常/死淘/丢失/出栏 |
| `entryWeightKg` | decimal | 入栏体重 |
| `source` / `breedingMethod` / `entryCycle` | string | 来源、养殖方式、周期 |
| `realtimeTemp` / `realtimeSteps` / `envTemp` | number | IoT 快照 |
| `fatherSubjectCode` / `motherSubjectCode` | string | 系谱 pseudonym |

---

## 4. 鉴权流程

1. `ResearchOpenApiController` 标注 `@Anonymous` → Spring Security 放行。  
2. `ResearchOpenApiAuthFilter` 拦截 `/open-api/v1/research/*`（**排除** `/auth/token`）。  
3. 解析 `Authorization: Bearer …`，`ResearchOpenApiTokenService.validateToken` 验签与过期。  
4. 失败返回 JSON `{ code:401, message:"..." }`，HTTP 401。

Token 申请：`appKey`/`appSecret` 与配置项精确匹配后签发 JWT（HS512 + `token-secret`）。

---

## 5. 数据查询

复用既有 Service / Mapper：

| 接口 | Service | 表 | 条件 |
| --- | --- | --- | --- |
| 牧场列表 | `IBizPastureService.selectBizPastureList` | `biz_pasture` | `del_flag=0` |
| 档案列表 | `IBizYakAssetService.selectBizYakAssetList` | `biz_yak_asset` | `del_flag=0`；支持 `assetStatus`、`gender`、`pastureId` |

实体 → VO：`ResearchOpenApiSupport.toPastureVo` / `toYakAssetVo`（内部调用 `ResearchOpenApiMasking`）。

---

## 6. 脱敏实现

| 方法 | 说明 |
| --- | --- |
| `maskPhone` | 复用 `SubsidyGrantRecordMasking.maskPhone` |
| `maskPersonName` | 首字 + `*` |
| `regionSummary` | 区县 + 乡镇 |
| `roundCoordinate` | 经纬度 3 位小数 |
| `subjectCode` | `YS` + 8 位 `id` |
| `pseudonymCode` | 系谱编号哈希短码 |

---

## 7. 异常码

| code | HTTP | 场景 |
| --- | --- | --- |
| 200 | 200 | 成功 |
| 401 | 401 | appKey/appSecret 错误、Token 缺失/无效/过期 |
| 400 | 200* | pageSize 超限（body.code=400） |
| 503 | 503 | `enabled=false` 或未配置凭证 |

\* 业务错误仍返回 JSON body，HTTP 状态与 `code` 一致（Filter 401/503；Controller 内 ServiceException 写入 body.code）。

---

## 8. 调用示例

```bash
# 1. 申请 Token
curl -X POST "http://localhost:8010/open-api/v1/research/auth/token" \
  -H "Content-Type: application/json" \
  -d '{"appKey":"research-open-demo-key","appSecret":"research-open-demo-secret-change-me"}'

# 2. 牧场列表
curl "http://localhost:8010/open-api/v1/research/pastures?pageNum=1&pageSize=10" \
  -H "Authorization: Bearer <accessToken>"

# 3. 牦牛档案
curl "http://localhost:8010/open-api/v1/research/yak-assets?pageNum=1&pageSize=10&assetStatus=1" \
  -H "Authorization: Bearer <accessToken>"
```

---

## 9. 测试

见同目录 `科研开放数据接口测试用例.md`。单元测试：

- `ResearchOpenApiMaskingTest`
- `ResearchOpenApiTokenServiceTest`
- `ResearchOpenApiControllerApiTest`

---

## 10. 需求追溯

| 功能需求 | 技术落点 |
| --- | --- |
| §5 认证 | **§2** 配置、**§4** Filter + TokenService |
| §6.2 牧场 | **§3.2**、**§5** BizPasture |
| §6.3 档案 | **§3.3**、**§5** BizYakAsset |
| §7 脱敏 | **§6** Masking |
| §3 不实现机构区分 | 单一 app-key/secret，JWT 无 clientId |

---

## 11. 修订记录

| 版本 | 日期 | 说明 |
| --- | --- | --- |
| 1.0 | 2026-07-04 | 初版：Token + 牧场/档案脱敏列表；全局一对凭证 |
