# 定义第三方服务（ThirdServiceSchema）

第三方服务适配器契约（如微信支付 tenpay、短信、文件存储）。`defineThirdService` 声明适配器类契约：构造函数配置 + 方法列表。

> 完整对接流程（前置准备 → 契约化 → gen third → 填充 client → 沙箱验证）见 [methodology/third-party-integration.md](../../docs/methodology/third-party-integration.md)。

与两个相近概念区分：

- `ServiceSchema`（service_schema/）——后端业务服务；
- `ThirdApiSchema`（project.config.ts `thirdApis`）——项目拓扑：第三方系统的目录归属，如 `wx/`、`ble/`。

`ThirdServiceSchema.schema` 引用 `ThirdApiSchema` 实例（拓扑引用）。第三方方法实现在外部系统，仅声明契约、不建模内部流程。

## 定义

```ts
import { buildInput, buildOutput, CodeException, defineThirdService, dtoField, intField, IOException, stringField } from '@pylonts/dsl';
import { wx } from '../project.config';

const totalFee = intField({ optional: false, label: '金额（分）' });

export const wxPayService = defineThirdService({
  schema: wx,
  name: 'WxPayService',
  description: '微信支付服务（tenpay APIv2）',
  methods: {
    getPayParams: {
      args: buildInput('PayParams', {
        out_trade_no: dtoField(stringField({ maxLength: 32, optional: false, label: '订单号' })),
        total_fee: dtoField(totalFee),
        // Same fact and same type as the entity column — shared instance.
        openid: dtoField(user.columns.openid),
      }),
      results: buildOutput('PayParamsResult', {
        appId: dtoField(stringField({ optional: false, label: 'appId' })),
        paySign: dtoField(stringField({ optional: false, label: '签名' })),
      }),
      throws: [CodeException, IOException],
      description: '获取支付参数',
    },
  },
});
```

- `methods` 是 **map**：key 即方法名（构建器写回 `method.name`），value 为 `ThirdServiceMethodDef`。
- 每个方法的 `args` / `results` 各是一个 `DtoMessage`，用 `buildInput` / `buildOutput` 构建。`buildInput` 构建 `args`（输入消息），`buildOutput` 构建 `results`（输出消息）——与业务 DTO 同一 POJO 聚合，字段以 `dtoField(field)` 包装，map key 即线格式（wire-format）字段名，**原样保留协议拼写**（`out_trade_no`、`appId`，不做 camelCase）。
- `throws`（**必填**）：每个方法必须声明 **`CodeException` + `IOException`** 两个异常——`CodeException`（第三方返回的业务错误码）与 `IOException`（网络/超时/不可恢复故障）。两个异常都来自 `@pylonts/core`，**不能自定义、不能替换**（`pylonts gen third` 会校验，缺失即报错拒绝生成）。原因：第三方集成有两类必然失败——业务层失败（第三方返回错误码，需转译给调用方）与传输层失败（网络/超时，需按故障重试或上报），client 骨架的异常翻译依赖这两个契约。
- `format`（可选）：网关**数据格式**——`'form'`（urlencoded）/ `'json'`（默认）/ 自定义字符串。client（出站加密/签名）与 sandbox（入站解密/验签）是同一协议的一对镜像，格式在声明中指定一次，两边的生成骨架都读它。
- 方法级 `service`（可选）：发往网关的 **wire service 名**（如方法 key `uploadImage` → `service: 'pic_upload'`），默认 = 方法 key。camelCase key 与 snake_case wire 名不一致时必须显式声明——sandbox endpoint 的路由靠它匹配。
- `description`（可选）：服务或方法说明。

字段与本地实体列/其他消息字段的关系，两个通道，按"同一事实"的表达方式选择：

### 同一概念且类型一致 → 共享实例

直接复用本地实体列实例，类型/语义/默认值自动跟随实体，DTO 投影继承全部语义：

```ts
args: {
  name: 'PayParams',
  fields: {
    openid: dtoField(user.columns.openid),   // user 是 TableSchema 实例，此处复用其 openid 列的 Field 对象
  },
},
```

线格式字段名（map key）与列名无需一致——key 是协议拼写，value 是任意 Field 实例，两者解耦。协议叫 `userId`、列叫 `user_id` 照样共享：

```ts
fields: {
  userId: dtoField(user.columns.user_id),   // key 按协议拼写，value 复用列实例
},
```

> `user` 是 `schema/user.table.ts` 中 `export const user = defineTable('user', { ... })` 导出的 **TableSchema 实例**（`user.columns` 是它的列 map，`user.columns.openid` 是该表 `openid` 列的 Field 实例）。共享实例即把**同一个 Field 对象**放入消息字段，类型/语义/默认值全部跟随表定义。

### 同一概念但类型/格式不同 → 自有字段

声明自有线格式类型：

```ts
const totalFee = intField({ optional: false, label: '金额（分）' });

fields: {
  total_fee: dtoField(totalFee),
},
```

wire 字段与本地字段之间的换算/映射（分↔元、加密、脱敏）由 convert 防腐层承载，`FieldRuleSchema` 换算规则为规划能力、尚未接入消息绑定。

## 嵌套字段

线格式字段支持递归嵌套，用 `objectField` / `arrayField`（Field 体系，非表列）：

```ts
fields: {
  payer_info: dtoField(objectField({
    properties: {
      openid: stringField({ optional: false, maxLength: 64 }),
    },
  })),
  coupons: dtoField(arrayField({ items: intField() })),
},
```

表列不支持这两个类型（`buildCreateTableSql` 直接报错，定义期即拦截）。

## 枚举与异常

第三方消息字段可用 `enumField` 挂 `defineEnum` 枚举，两者与 `defineThirdService` 定义在同一源文件中（named export），供 `pylonts gen third` 生成枚举产物。

异常不在此处定义——方法 `throws` 固定声明 `@pylonts/core` 的 `CodeException` + `IOException`（见上文「定义」一节），`pylonts gen third` 强校验。

## 回调（callbacks，第三方 → 平台推送）

`defineThirdService` 的 `callbacks` 字段建模**入站推送**：第三方审核完成/状态变更后主动 POST 到平台（如银联自助签约 3.8 入网审核结果推送、3.12 变更推送）。与 `methods`（平台 → 第三方出站）方向相反，语义是"第三方发来报文，平台应答"。

```ts
import { buildInput, buildOutput, defineThirdCallback, defineThirdService, dtoField, stringField } from '@pylonts/dsl';
import { api, unionpay } from '../project.config';

export const unionpaySigningService = defineThirdService({
  schema: unionpay,
  name: 'UnionpaySigningService',
  methods: {
    // ... 出站方法（3.1 图片上传等）
  },
  callbacks: {
    applyNotify: defineThirdCallback({
      api,                                   // 生成到 api/src/modules/unionpay/controller/
      service: 'apply_notify',               // 第三方推送的 service 名（固定传输值）
      payload: buildInput('ApplyNotifyPayload', {
        ums_reg_id: dtoField(stringField({ maxLength: 30, optional: false })),
        apply_status: dtoField(enumField({ enum: ApplyStatus, optional: true })),
        // ... 推送报文字段
      }),
      response: buildOutput('NotifyResponse', {
        res_code: dtoField(stringField({ maxLength: 10, optional: false, label: '0000 成功' })),
      }),
      description: '入网审核结果推送',
    }),
  },
});
```

- `callbacks` 是 **map**：key 即回调方法名（构建器写回 `callback.name`），value 由 **`defineThirdCallback`** 构建。
- **`api`（必填）**：回调 controller 生成进哪个后端——`{api}/src/modules/{thirdApi.name}/controller/{ServiceName}CallbackController.ts`。这是 **@Public 白名单目录**（`lint controller`：`src/modules/{thirdApi}/controller/` 下允许 @Public，thirdApi 名来自 project.config.ts）——第三方调用不持有平台 appKey，平台在 controller 方法体内验第三方的自有签名。
- **`service`（必填）**：第三方推送的服务名（如 `apply_notify`、`alter_notify`），供回调实现按 service 分发。
- **`payload` / `response`**：各是一个 `DtoMessage`（`buildInput`/`buildOutput`），与 `methods` 的 args/results 同一套 POJO 聚合，wire 字段名原样保留协议拼写；`payload` 是第三方推来的报文，`response` 是平台应答报文（如 `{ res_code: '0000' }`）。
- `description`（可选）：回调说明，渲染为 `@Response` 的 OpenAPI 描述。

生成物（`pylonts gen third` 一并产出）：

| 产物 | 路径 | 性质 |
|---|---|---|
| payload/response TypeBox 消息 | `third/{thirdApi}/{stem}.third-service.gen.ts`（与出站 DTO 同文件） | 覆盖写 |
| 回调 controller 骨架 | `{api}/src/modules/{thirdApi}/controller/{ServiceName}CallbackController.ts` | 已存在合并（用户方法体保留），`--force` 覆盖 |

回调 controller 是 `@Rpc('{serviceStem}Callback')` + 每方法 `@Public()` + `@Body(payload)` + `@Response(desc, response)` 的 RPC 类；**方法体是手写面**：验第三方签名/解密（与出站 client 的密钥对称）、按 `service` 分发、落库/通知业务，最后返回 `response` 消息。RPC 路由为 `POST /{urlPrefix}/{rpcName}/{method}`，第三方按文档推送即可。

## 存储与生成

- 声明：`third_schema/{thirdApi.name}/*.third-service.ts`——一文件一服务（named export），目录名 = project.config.ts 的 `thirdApis` 实例名。
- 生成：`pylonts gen third`——对每个 thirdApi，扫描 `third_schema/{name}/`，按三步产出到 `third/{name}/`：
  0. **throws 校验**（生成前置闸门）：每个方法必须声明 `CodeException` + `IOException`，缺失即报错列出违规方法，不写任何产物；
  1. **枚举**：模块导出的 `defineEnum` 实例 → `third/{name}/enums/{JsName}.enum.ts`（复用 enum-driver 的 `renderEnum`，与表枚举同一渲染），一 jsName 一文件；
  2. **DTO**：每方法 args/results 用 typebox-driver 渲染 TypeBox 消息 + Static 类型，**一源文件一生成文件**，输出 `third/{name}/{stem}.third-service.gen.ts`（覆盖写）；
  3. **客户端骨架**：每服务渲染一个 class（构造配置接口 + 每方法 async 签名 + Not-implemented throw），输出 `third/{name}/{stem}.client.ts`——已存在则**合并**（用户填充的方法体保留、签名/DTO import 刷新），`--force` 覆盖；
  4. **沙箱网关配置**（可选）：每 thirdApi 渲染 `sandbox/{name}/config.ts`（defineSandboxConfig 骨架）——每方法一个 endpoint（`service` 名 = 声明的 wire service、`format` = 声明的数据格式、`method` 默认 POST、handler stub `res_code: '0000'`），已存在则**合并**（用户填充的 handler 保留、契约新增接口自动追加、移除接口删除），`--force` 覆盖；**无生成标记的手写 config 保留不动**（cli 打印 `- (hand-written, delete to regenerate)` 提示）。encoder（解密/验签）与 GATEWAY baseUrl 是手写面，不在生成范围。
- DTO 的枚举字段 import 走**相对路径** `./enums/{JsName}.enum`（DTO 与 enums/ 同处 `third/{name}/` 下，`moduleResolution: bundler` 解析 `.enum.ts`），不依赖根 `enums/` 子包。
- 生成物目录是子包：`third/` 目录带 `package.json`，`exports` 声明 `*.third-service.gen` 子路径，供 convert 产物 import。

## 客户端骨架是生成的，方法体是手写的

`defineThirdService` 只描述**契约**（构造配置 + 方法列表）。`gen third` 生成的 `{name}.client.ts` 是一个**骨架**：导出 `{Service}Config` 接口（TODO 注释标注 transport 配置——baseUrl/凭据/密钥属外部实现，不在契约内）+ `{Service}` 类（constructor 空实现），每方法带完整签名（args/results 类型从同名 `.third-service.gen` import type）与 `throw new Error('Not implemented: ...')` stub（含 `// @gen:stub` 标记，与 gen-service 骨架同一套 marker 约定）。**签名/throws 注释由生成器保证与契约同步，方法体、构造配置、签名加密等外部交互由人工填充**——已存在文件默认跳过（避免覆盖人工实现），`--force` 才重写。

## convert 防腐接线

第三方消息（`args`/`results` 是 `DtoMessage`，天然满足 `ConvertSourceSchema`）可直接作为 convert 的**源或目标**，用于 wire 消息 ↔ 本地模型的防腐翻译。```ts
// convert_schema/{api.name}/{app.name}/convert/wx-pay.convert.ts
import { wxPayService } from '../../../third_schema/wx/wxpay.third-service';

const getPayParams = wxPayService.methods.getPayParams;

export const wxPayConvert = defineConvert({
  name: 'WxPayConvert',
  api,
  app: admin,
  methods: {
    toPayParams: {
      sources: [order],                 // 本地订单表 → wire 请求
      target: getPayParams.args,
    },
    toLocalPayResult: {
      sources: [getPayParams.results],  // wire 响应 → 本地 DTO
      target: WxPayParamsResultDto,
    },
  },
});
```

convert 文件绑定第三方服务身份时按 `{third-service}.convert.ts` 命名（上例 `wx-pay.convert.ts` 对应 `wxpay.third-service.ts` 的 `WxPayService`）。