# 接口Service、接口方法、WebSocket服务使用手册

本文档聚焦 `typescript-boot` 中最核心的三类能力：

- HTTP 接口服务，也就是“接口Service”
- HTTP 接口方法，也就是“接口Service”中的业务方法
- WebSocket 服务

目标不是讲全量导出工具，而是把这三类能力从安装、配置、编码、发布到部署的主流程说明白。

如果你想先把服务跑起来，再逐步接入数据库、登录态、Redis、邮件等能力，建议先读本文档。

## 1. 最小准备

### 1.1 安装

```bash
npm install typescript-boot reflect-metadata
```

### 1.2 TypeScript 配置

至少需要打开下面两个选项：

```json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
```

### 1.3 入口文件要求

程序入口文件最先执行：

```ts
import 'reflect-metadata';
```

### 1.4 推荐脚本

`typescript-boot` 是运行时库，不自带项目 CLI。通常建议你自己的业务项目这样配置：

```json
{
  "scripts": {
    "dev": "ts-node src/index.ts",
    "build": "tsc -p tsconfig.json",
    "start": "node dist/index.js"
  }
}
```

## 2. SeeksWebServer 是发布入口

无论是 HTTP 接口服务还是 WebSocket 服务，最终都要通过 `SeeksWebServer` 发布。

最小启动代码：

```ts
import 'reflect-metadata';
import {SeeksWebServer} from 'typescript-boot';

const server = new SeeksWebServer(3333);
server.start();
```

常用写法：

```ts
server.publishService(new XxxService());
server.publishSocketService(new XxxSocketService());
server.start();
```

可选项示例：

```ts
const server = new SeeksWebServer(3333, {
  disableDoc: false,
  jsonBodyLimit: '20mb',
  frontRootPath: './dist'
});
```

常用配置项：

- `disableDoc`：关闭内置在线文档
- `jsonBodyLimit`：JSON body 大小限制
- `frontRootPath`：静态前端目录
- `proxyTable`：代理扩展点

常用实例方法：

- `publishService(service)`
- `publishSocketService(service)`
- `setFrontRootPath(path)`
- `setFontRootPath(path)`
- `setProxyTable(proxyTable)`
- `getAppService()`
- `getServer()`
- `start()`

## 3. 什么是“接口Service”

“接口Service”就是一个继承 `BaseService` 的类，它代表一组共享同一命名空间的 HTTP 接口。

示例：

```ts
import {
  BaseService,
  NoRequiredPermission,
  apiDoc,
  apiPath,
  apiPermission
} from 'typescript-boot';

@apiDoc('账号服务')
@apiPermission(NoRequiredPermission)
@apiPath('/account')
export class AccountService extends BaseService {
}
```

这里最重要的 3 个点：

- `@apiPath()` 必须有，它定义 `service-path`
- `@apiDoc()` 建议有，它决定文档中的服务名称
- `@apiPermission()` 可选，用来定义 service 级默认权限

当前版本中，`service-path` 只能是单段路径。

允许：

```ts
@apiPath('/account')
@apiPath('account')
```

不允许：

```ts
@apiPath('/account/oauth')
```

框架会在服务启动注册阶段报错。

## 4. 什么是“接口方法”

“接口方法”就是 `BaseService` 子类中的普通实例方法。框架会把它们注册成真实的 HTTP 路由。

示例：

```ts
import {
  BaseService,
  apiDoc,
  apiParamFromBody,
  apiPath,
  apiReturn
} from 'typescript-boot';

@apiPath('/account')
export class AccountService extends BaseService {
  @apiDoc('登录')
  @apiReturn('登录结果')
  @apiPath('login')
  async login(
    @apiParamFromBody('登录账号') account: string,
    @apiParamFromBody('登录密码') password: string
  ) {
    return this.success({
      account,
      token: 'demo-token'
    });
  }
}
```

发布后，请求路径是：

```text
/account/login
```

这里要特别注意一个当前版本规则：

- `BaseService` 子类中的普通实例方法，默认都会被视为接口方法
- 因此不要把辅助函数直接写在 Service 类里
- 辅助逻辑请放到 `manager/dao/utils` 或其他普通类中

## 5. 完整 HTTP 示例

```ts
import 'reflect-metadata';
import {
  BaseService,
  LoginRequiredPermission,
  NoRequiredPermission,
  SeeksWebServer,
  apiDoc,
  apiParamFromBody,
  apiPath,
  apiPermission,
  apiRequest,
  apiRequestType,
  apiReturn
} from 'typescript-boot';

@apiDoc('账号服务')
@apiPermission(NoRequiredPermission)
@apiPath('/account')
class AccountService extends BaseService {
  @apiDoc('登录')
  @apiReturn('登录结果')
  @apiPath('login')
  @apiRequestType('POST')
  async login(
    @apiParamFromBody('登录账号') account: string,
    @apiParamFromBody('登录密码') password: string
  ) {
    return this.success({
      account,
      token: 'demo-token'
    });
  }

  @apiDoc('获取当前用户信息')
  @apiPermission(LoginRequiredPermission)
  @apiReturn('当前登录用户')
  @apiPath('profile')
  @apiRequestType('GET')
  async profile(@apiRequest() req) {
    return this.success({
      token: req.headers.token || req.headers.authorization || ''
    });
  }

  @apiDoc('根据 provider 获取授权地址')
  @apiPath('oauth/:provider/authorizeUrl')
  @apiRequestType('GET')
  async getAuthorizeUrl(@apiRequest() req) {
    return this.success({
      provider: req.params.provider
    });
  }
}

const server = new SeeksWebServer(3333);
server.publishService(new AccountService());
server.start();
```

## 6. 路径规则

一个接口方法的完整访问路径为：

```text
http://<hostname>:<port>/<service-path>/<method-path>
```

### 6.1 `service-path` 规则

- 写在 `接口Service` 的 `@apiPath()` 上
- 只能是单段路径
- 会自动规范化为带前导 `/` 的形式

例如：

- `account` 会变成 `/account`
- `/account/` 会变成 `/account`

### 6.2 `method-path` 规则

`method-path` 写在接口方法的 `@apiPath()` 上，当前版本支持：

- 单段路径
- 多段路径
- 变量路径
- 空路径

示例：

```ts
@apiPath('login')
@apiPath('oauth/github/authorizeUrl')
@apiPath('oauth/:provider/authorizeUrl')
@apiPath('')
```

### 6.3 默认路径

如果接口方法没有写 `@apiPath()`，则默认使用方法名作为 `method-path`。

例如：

```ts
@apiPath('/account')
class AccountService extends BaseService {
  async login() {}
}
```

最终路径：

```text
/account/login
```

### 6.4 变量路径的读取方式

当前版本没有 `@apiParamFromPath()`。

如果使用了变量路径：

```ts
@apiPath('oauth/:provider/authorizeUrl')
```

请通过 `@apiRequest()` 读取 `req.params`：

```ts
async getAuthorizeUrl(@apiRequest() req) {
  return this.success({
    provider: req.params.provider
  });
}
```

更多细节可参考：

- [api-path-rules.md](./api-path-rules.md)

## 7. 请求方式规则

可以用 `@apiRequestType('GET')` 或 `@apiRequestType('POST')` 指定请求方式。

支持写在：

- service 上，作为默认值
- method 上，覆盖 service 默认值

如果都不写，则该接口会同时注册：

- `GET`
- `POST`

同时，框架会为已注册的接口路径自动补上对应的 `OPTIONS`，用于预检请求。

当前版本只有 `GET` 和 `POST` 两种业务请求方式内置支持。

## 8. 参数获取方式

### 8.1 推荐使用参数装饰器

常用参数装饰器如下：

- `@apiParamFromBody()`：从 `req.body` 取单个字段
- `@apiBodyAsParam()`：把整个 body 转为对象，或从 body 解构多个字段
- `@apiParamFromQuery()`：从 `req.query` 取字段
- `@apiParamFromHeader()`：从 `req.headers` 取字段
- `@apiRequest()`：直接拿原始 `req`
- `@apiResponse()`：直接拿原始 `res`
- `@apiSessionUser()`：拿当前登录用户
- `@apiParamsState()`：拿一个空对象，通常只在少量高级场景使用

### 8.2 `@apiParamFromBody()`

```ts
async login(
  @apiParamFromBody('登录账号') account: string,
  @apiParamFromBody('登录密码') password: string
) {
}
```

### 8.3 `@apiBodyAsParam()`

把整个 body 当成对象：

```ts
async save(
  @apiBodyAsParam('用户对象') user: UserVO
) {
}
```

也支持解构：

```ts
async save(
  @apiBodyAsParam('用户名和年龄') {name, age}: UserVO
) {
}
```

### 8.4 `@apiParamFromQuery()`

```ts
async page(
  @apiParamFromQuery('页码') currentPage: number,
  @apiParamFromQuery('分页大小') pageSize: number
) {
}
```

### 8.5 `@apiParamFromHeader()`

```ts
async checkVersion(
  @apiParamFromHeader('客户端版本') clientVersion: string
) {
}
```

### 8.6 `@apiRequest()` 和 `@apiResponse()`

适合下面这些场景：

- 读取 `req.params`
- 直接读取原始 header
- 处理上传流
- 自己控制响应输出

### 8.7 `@apiSessionUser()`

前提是：

- 该接口开启了登录态校验
- 启动时已经调用 `setupSessionManager(...)`

### 8.8 一个关键约定

当前实现中有两种调用模式：

1. 方法完全不使用参数装饰器
2. 方法使用了参数装饰器

如果完全不使用参数装饰器，框架会按旧风格把参数当成：

```ts
async method(req, res, sessionUser) {}
```

如果使用了参数装饰器，则只有被装饰的参数位置会被注入。没有装饰器的参数位置不会自动补值。

因此，只要你的方法里已经用了：

- `@apiParamFromBody()`
- `@apiRequest()`
- `@apiSessionUser()`

这类装饰器，就不要再假设未装饰参数会自动拿到 `req/res/sessionUser`。

### 8.9 当前版本的参数限制

当前版本还没有：

- 自动参数校验
- query/body 到 `number/boolean/date` 的自动转换
- `@apiParamFromPath()`

也就是说：

- `query` 参数默认仍然是字符串
- 类型注解主要用于编辑器提示和文档描述
- 需要的转换和校验请在业务代码中手动完成

## 9. 返回结果和异常

### 9.1 推荐返回 JSON

成功：

```ts
return this.success(data);
```

失败：

```ts
return this.fail('错误信息');
```

### 9.2 抛出业务异常

```ts
throw new WarningError('提示信息');
throw new CodeError(403, '无权限');
```

也可以用快捷方法：

```ts
throwWarningErrorIf(!id, 'id不能为空');
```

### 9.3 返回文本

直接返回字符串即可：

```ts
return 'ok';
```

### 9.4 下载文件

```ts
async exportFile() {
  const buffer = Buffer.from('hello');
  return this.downloadFile('demo.txt', buffer);
}
```

### 9.5 自己完成响应输出

如果你自己使用原始 `res` 完成了输出，请返回：

```ts
return this.streamed();
```

## 10. 权限、文档和日志

### 10.1 权限

常用写法：

```ts
@apiPermission(NoRequiredPermission)
@apiPermission(LoginRequiredPermission)
@apiPermission({
  login: true,
  roles: ['ADMIN']
})
```

权限可以写在：

- service 上，作为默认权限
- method 上，覆盖 service 默认权限

### 10.2 文档注解

常用注解：

- `@apiDoc()`：服务名、方法名、对象属性说明
- `@apiReturn()`：返回值说明
- `@apiDocArrayItem()`：数组项类型
- `@apiDocEnum()`：枚举说明
- `@apiHidden()`：把整个 service 从在线文档中隐藏

这里要注意：

- `@apiHidden()` 只是让该 service 不出现在文档里
- 它不会阻止该 service 被真正发布

### 10.3 `@apiLogToDB()`

如果要让框架在接口调用后自动写行为日志，可以使用：

```ts
@apiLogToDB()
```

如果你想定制写入内容：

```ts
@apiLogToDB(async(id: number, sessionUser) => {
  return {
    behavior_desc: '删除用户',
    request_params: JSON.stringify({id}),
    account: sessionUser && sessionUser.account
  };
})
async removeUser(
  @apiParamFromBody('用户ID') id: number,
  @apiSessionUser() sessionUser
) {
  return this.success(true);
}
```

这里的关键点是：

- `apiLogToDB` 的回调参数，不是固定的 `req/res/sessionUser`
- 它收到的是该接口方法在运行时真正注入后的参数列表
- 如果你希望在回调里拿到 `req` 或 `sessionUser`，就需要把它们本身声明为方法参数

要让自动日志真正生效，还需要在启动阶段调用：

```ts
setupServiceLogManager(...)
```

## 11. WebSocket 服务

WebSocket 服务用于处理长连接场景，例如：

- 实时通知
- 扫码登录状态推送
- 导入进度推送
- 聊天
- 实时看板

WebSocket 服务需要继承：

```ts
BaseSocketService
```

### 11.1 连接路径

WebSocket service class 上的 `@apiPath()` 是连接路径。

例如：

```ts
@apiPath('/chat')
class ChatSocketService extends BaseSocketService {}
```

客户端连接地址：

```text
ws://localhost:3333/chat
```

规则和 HTTP Service 一样：

- class 上的 `@apiPath()` 必须有
- 只能是单段路径

### 11.2 动作路径

WebSocket 方法支持：

- 不写 `@apiPath()`，默认用方法名作为 `action`
- 写 `@apiPath('echo')`
- 写多段 `action`，例如 `@apiPath('room/join')`

当前版本中，WebSocket service 里的普通实例方法同样会被视为可调用动作。

因此建议：

- 业务动作保留在 `BaseSocketService` 子类里
- 辅助函数放到其他普通类
- 客户端协议尽量只使用你显式设计好的 `action`

### 11.3 消息协议

客户端发送：

```json
{
  "action": "echo",
  "requestId": "r1",
  "data": {
    "text": "hello"
  }
}
```

字段含义：

- `action`：动作名
- `requestId`：可选，请求唯一标识，服务端会原样带回
- `data`：业务数据

成功响应示例：

```json
{
  "action": "echo",
  "requestId": "r1",
  "code": 200,
  "success": true,
  "data": {
    "text": "hello"
  }
}
```

失败响应示例：

```json
{
  "action": "echo",
  "requestId": "r1",
  "code": 500,
  "error": true,
  "message": "错误原因"
}
```

### 11.4 WebSocket 方法签名

动作方法签名约定为：

```ts
async someAction(client, data, request) {
}
```

三个参数分别是：

- `client`：当前连接对应的 `SocketClient`
- `data`：客户端消息中的 `data`
- `request`：客户端发送的完整原始消息对象

WebSocket 动作不走 HTTP 那套参数装饰器注入机制。

### 11.5 生命周期

`BaseSocketService` 提供 4 个主要钩子：

- `authorizeConnection(client, req)`：连接建立后首先执行，可用于校验 token 和绑定用户信息
- `onConnected(client, req)`：连接成功后执行
- `onDisconnected(client, code, reason)`：连接关闭时执行
- `onError(client, error)`：连接出错时执行

如果 `authorizeConnection()` 返回值不是 `undefined`，框架会把它放到：

```ts
client.userInfo
```

### 11.6 `SocketClient` 常用能力

- `client.id`：连接 ID
- `client.userInfo`：连接鉴权后绑定的用户信息
- `client.state`：当前连接的临时状态
- `client.send(message)`：发送字符串或对象
- `client.sendAction(action, data, requestId)`：发送标准成功消息
- `client.close(code, reason)`：主动关闭连接

### 11.7 `BaseSocketService` 常用能力

- `getClients()`
- `findClients(filterFn)`
- `sendToClient(client, message)`
- `broadcast(message, filterFn?)`
- `sendToUser(userId, message, userIdGetter?)`

## 12. 完整 WebSocket 示例

```ts
import 'reflect-metadata';
import {
  BaseSocketService,
  SeeksWebServer,
  apiPath
} from 'typescript-boot';

@apiPath('/chat')
class ChatSocketService extends BaseSocketService {
  async authorizeConnection(client, req) {
    const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
    const userId = url.searchParams.get('userId') || 'anonymous';
    return {userId};
  }

  async onConnected(client) {
    client.sendAction('connected', {
      clientId: client.id,
      userId: client.userInfo.userId
    });
  }

  async onDisconnected(client, code, reason) {
    console.log('socket disconnected:', client.id, code, reason);
  }

  async ping(client, data) {
    return {
      pong: data
    };
  }

  @apiPath('room/join')
  async joinRoom(client, data) {
    client.state.roomId = data.roomId;
    return this.success({
      roomId: data.roomId
    });
  }

  @apiPath('room/send')
  async sendRoomMessage(client, data) {
    this.broadcast({
      action: 'room/message',
      data: {
        from: client.userInfo.userId,
        roomId: client.state.roomId,
        message: data.message
      }
    }, currentClient => {
      return currentClient.state.roomId === client.state.roomId;
    });

    return this.success(true);
  }
}

const server = new SeeksWebServer(3333);
server.publishSocketService(new ChatSocketService());
server.start();
```

## 13. 同时发布 HTTP 与 WebSocket

```ts
const server = new SeeksWebServer(3333);
server.publishService(new AccountService());
server.publishService(new OrderService());
server.publishSocketService(new ChatSocketService());
server.start();
```

虽然 `publishService(new ChatSocketService())` 当前也能被自动识别为 WebSocket 服务，但更推荐显式使用：

```ts
server.publishSocketService(new ChatSocketService());
```

这样代码更清晰。

## 14. 在线文档

默认情况下，启动后会自动发布在线文档：

```text
http://localhost:3333/typescript-boot
```

文档数据接口：

```text
http://localhost:3333/typescript-boot/docs/getServiceList
```

如果不希望暴露在线文档：

```ts
const server = new SeeksWebServer(3333, {
  disableDoc: true
});
```

## 15. 从开发到发布的推荐流程

### 15.1 本地开发

1. 在入口文件最前面引入 `reflect-metadata`
2. 创建 `SeeksWebServer`
3. 发布你的 HTTP Service 和 WebSocket Service
4. 本地使用 `ts-node` 或 `tsx` 直接启动

示例：

```bash
npm run dev
```

### 15.2 构建

```bash
npm run build
```

构建后，一般产物会在：

```text
dist/
```

### 15.3 启动生产包

```bash
npm run start
```

### 15.4 部署建议

- HTTP 与 WebSocket 都走同一个 `SeeksWebServer` 端口即可
- 如果前面有 Nginx/Caddy，请放行 WebSocket `Upgrade` 和 `Connection` 头
- 单机部署优先考虑 `SessionManagerByFS`
- 多节点部署优先考虑 `SessionManagerByRedis`
- 如果你有前端 SPA，可使用 `frontRootPath` 直接发布静态资源

## 16. 常见坑位

### 16.1 不要把辅助方法写在 Service 类里

当前版本中，普通实例方法会被注册成 HTTP 路由或 WebSocket 动作。

### 16.2 `service-path` 只能单段

不要写：

```ts
@apiPath('/account/oauth')
```

需要多段路径时，把多段部分放到 `method-path`。

### 16.3 需要路径变量时，请通过 `req.params` 读取

当前版本没有 `@apiParamFromPath()`。

### 16.4 参数不会自动转换

例如：

- `@apiParamFromQuery()` 取出来通常仍然是字符串
- 需要数值或布尔值时，请手动转换

### 16.5 对象映射依赖属性元数据

如果你希望：

- 在线文档能正确展示对象结构
- `@apiBodyAsParam()` / 数据库对象映射更稳定

建议在对象属性上补充 `@apiDoc()` 等元数据注解。

## 17. 相关文档

- [typescript-boot-user-manual.md](./typescript-boot-user-manual.md)
- [api-path-rules.md](./api-path-rules.md)
- [from-author.md](./from-author.md)
