# 模块服务间调用协议

通过模块属性绑定 ServiceReference，插件按新协议领取当前凭据并调用同一 Runtime 中的目标模块方法。

模块 A 的独立后端需要调用同一 Runtime 中模块 B 的公开方法时，在模块 A 声明 `Interface` 属性，
把该属性设置为指向 B 的 `ServiceReference`。Runtime 自动生成来源服务到目标服务的访问关系和 token。
插件按[生命周期与引用协议](/development/lifecycle-plugin-development/reference-protocol/)取得当前授权引用，
后端通过 Runtime 网关调用 B。

> **业务操作前按需解析引用访问**
>
> 生命周期仅交付无 token 的引用描述；调用凭据通过引用访问解析协议获得，仅用于当前操作，不持久化。
> 插件开发、注册、阶段处理和安装隔离统一见[生命周期插件开发](/development/lifecycle-plugin-development/)。

## 完整调用链

```text
目标 Bundle 通过 dependencies 先安装
  -> 模块 A 声明 Interface 属性
  -> 模块 A 的属性设置为 ServiceReference(B)
  -> Runtime 校验绑定并收敛访问关系
  -> 插件保存新生命周期请求的安装定位与无凭据引用描述
  -> 每次业务操作前按授权 propertyKey 解析当前引用的访问信息
  -> 后端在当前操作中通过 Runtime 网关调用 B 的目标方法
  -> Runtime 校验来源服务、目标服务和允许的方法
  -> 模块 B 执行方法并返回统一响应
```

Bundle `dependencies` 只保证目标 Bundle 进入安装闭包和先于调用 Bundle 安装，不选择目标服务，也不设置
调用模块属性。设置属性就是绑定，没有另一套需要开发者调用的授权接口。声明与 CLI 示例见
[ServiceReference 声明、绑定与运行时交付](/development/bundle-development/module-development/service-reference/)。

## 本次解析的访问信息

响应的 `data.reference` 保留当前逻辑引用；按其中的 `serviceId` 读取 `data.access` 对应条目：

```json
{
  "reference": {"@type": "ServiceReference", "serviceId": "<targetBusinessId>"},
  "access": {
    "<targetBusinessId>": {
      "url": "https://<runtime-service-base>/<targetBusinessId>",
      "token": "<opaque-reference-token>"
    }
  }
}
```

`url` 是 Runtime 网关中的目标服务基地址；`token` 是当前来源服务访问授权范围内目标方法的凭据。
它们由平台按当前绑定解析，调用方不得根据环境、域名或 `businessId` 自行拼接地址。
Map 使用 `ServiceReferenceMap.valueMap`；List 使用 `ServiceReferenceList.valueList`。先从逻辑结构选择目标，
再以 `serviceId` 读取调用信息。重复目标共用一份调用信息；任一目标无法授权时，本次解析失败。

HTTP 模块不能通过 `position: "property"` 接收引用。后端仅在当前操作中使用本次领取的凭据，
不得写入数据库、配置、日志、源码、Bundle 或前端状态。

## 调用具体方法

例如代码中的逻辑调用是：

```text
xiaoxiangService.listManagedShops(params)
```

外部后端使用本次按需领取的引用，把接口方法转换为以下 HTTP 请求：

```http
POST {access.url}/listManagedShops
Authorization: Bearer {access.token}
X-Runtime-Actor-Assertion: {currentRequest.actorAssertion}
Content-Type: application/json
```

`X-Runtime-Actor-Assertion` 只在当前入站 Runtime 调用已经建立用户身份时存在。调用方必须把它作为不透明、
请求级值转发；不得从生命周期 `userId`、业务参数或环境默认用户生成替代值。不需要用户身份的调用可以不带
该 Header。完整的单跳、链式轮换、异步边界和日志要求见
[Runtime 用户委托](/development/bundle-development/module-development/actor-delegation/)。

请求体顶层字段与目标方法当前 `paramDefinitions` 一一对应。目标方法声明一个名为 `params` 的参数时：

```json
{
  "params": {
    "status": "ACTIVE"
  }
}
```

平台当前不生成语言级代理对象；`xiaoxiangService.listManagedShops(params)` 表示上面的标准 HTTP 调用，
不是另一个未发布的 SDK。调用方应从 Runtime 回读目标方法定义，不能自行复制一份可能漂移的方法签名。
响应按 [CModel 错误模型](/integration/cmodel-error-model/) 处理。

调用方不得自行把逻辑 `ServiceReference` 转换成地址或凭据，不直接请求目标 Hosted Backend 的 Controller 或部署地址。

## 上下文、权限和重试

引用 token 证明当前来源服务被允许调用当前目标服务的方法，但不自动代表某个最终用户。需要用户权限时，
调用方还必须转发当前请求由 Runtime 注入的 Actor assertion：

- Runtime、工作区和来源服务身份由引用调用链确定，不从普通业务参数中接受同名字段。
- 生命周期请求中的操作 `userId` 不能保存后用于业务调用。
- 普通请求体或自定义头中的用户编号不是平台已验证的用户上下文。
- 目标方法需要最终用户权限而可信用户上下文缺失时，必须拒绝，不能回退到安装用户或固定管理员。
- Actor assertion 不能持久化或用于请求结束后的重试；服务链跨到新目标后，由 Runtime 为该目标后端自动
  轮换下一跳 assertion，外部服务不签名。

调用方对 HTTP `200` 响应检查 CModel `errorCode`；非 `200`、连接中断或超时时属于传输失败，执行结果可能未知。只有目标方法
本身声明幂等，或请求携带该方法规定的幂等键时，才允许自动重试。旧 token 返回未授权时不循环重试，
本次操作应失败，并检查活动安装和引用授权；不能回退到旧凭据或重新注册另一套外部服务。

## 可执行的端到端步骤

1. 在 Bundle `dependencies` 中声明目标 Bundle 的 Cargo 版本需求，并确认 Runtime 解析出的精确版本满足需求。
2. 在模块 A 当前版本中声明 `xiaoxiangService` 的 `Interface` 属性，并安装模块 A。
3. 回读模块 B 的完整 `businessId`、`listManagedShops` 方法及 `paramDefinitions`。
4. 使用正式的 `baijimu runtime app properties update` 命令，把模块 A 的 `xiaoxiangService` 设置为
   指向模块 B 的 `ServiceReference`；完整参数见 ServiceReference 页面，不要自行拼接平台接口路径。
5. 插件按新协议接收并校验安装定位，在业务操作前按授权属性领取当前地址和调用凭据，不要输出 token。
6. 由模块 A 后端按上述 JSON `POST` 请求调用 `listManagedShops`；当前请求存在 Actor Header 时同时原样
   转发，确认 HTTP `200` 并检查 CModel `errorCode`。
7. 修改或清除属性后，确认下一次领取反映当前绑定，撤销后的引用不能继续调用。

如果第 5 步领取失败，应检查安装环境、活动安装、插件身份、绑定协议、授权属性和当前目标关系；
不能用硬编码地址或非公开凭据绕过引用授权。
