# 插件后端工具与封装文档

本文档只整理 BuildingAI 插件/扩展后端可以直接使用的工具与封装。示例参考
`templates/extension-starter/src/api`。

## 导入总览

| 类型                          | 导入方式                                                   | 用途                                           |
| ----------------------------- | ---------------------------------------------------------- | ---------------------------------------------- |
| 基础 Controller/Service       | `@buildingai/base`                                         | 日志、分页结果、CRUD、事务、分页查询           |
| 插件 Controller/Entity 装饰器 | `@buildingai/core/decorators`                              | 插件前台接口、控制台接口、插件实体             |
| 通用装饰器                    | `@buildingai/decorators/...`                               | 公开接口、当前用户、文件 URL、响应转换         |
| DTO                           | `@buildingai/dto`                                          | 分页查询 DTO                                   |
| Pipe                          | `@buildingai/pipe/param-validate.pipe`                     | UUID 参数校验                                  |
| 错误封装                      | `@buildingai/errors`                                       | 统一业务错误                                   |
| 数据库                        | `@buildingai/db/@nestjs/typeorm`、`@buildingai/db/typeorm` | TypeORM Module、Repository、装饰器、查询操作符 |
| 平台实体                      | `@buildingai/db/entities`                                  | 使用平台已有实体，如 `User`                    |
| 插件 SDK                      | `@buildingai/extension-sdk`                                | 平台用户、AI、计费、tsup 构建配置              |
| 工具函数                      | `@buildingai/utils`                                        | where 构造、路径、类型转换、版本、文件、安全等 |


## 插件 Controller 装饰器

### ExtensionWebController

导入：

```ts
import { ExtensionWebController } from "@buildingai/core/decorators";
```

用途：声明插件前台 API Controller。

```ts
import { BaseController } from "@buildingai/base";
import { ExtensionWebController } from "@buildingai/core/decorators";
import { Public } from "@buildingai/decorators/public.decorator";
import { UUIDValidationPipe } from "@buildingai/pipe/param-validate.pipe";
import { Get, Param, Query } from "@nestjs/common";

@ExtensionWebController("article")
export class ArticleWebController extends BaseController {
    constructor(private readonly articleService: ArticleService) {
        super();
    }

    @Get()
    @Public()
    async findAll(@Query() query: QueryArticleDto) {
        return this.articleService.list(query);
    }

    @Get(":id")
    @Public()
    async findOne(@Param("id", UUIDValidationPipe) id: string) {
        return this.articleService.findOneById(id);
    }
}
```

可用参数：

| 写法                                                           | 说明                     |
| -------------------------------------------------------------- | ------------------------ |
| `@ExtensionWebController("article")`                           | 设置 Controller path     |
| `@ExtensionWebController({ path: "article" })`                 | 对象写法                 |
| `@ExtensionWebController({ path: "article", skipAuth: true })` | 整个 Controller 跳过鉴权 |

内置能力：

| 能力          | 说明                                           |
| ------------- | ---------------------------------------------- |
| 插件路由前缀  | 默认 `/{extensionIdentifier}/api/{path}`       |
| 环境前缀      | 如果存在 `VITE_APP_WEB_API_PREFIX`，使用该前缀 |
| 插件 metadata | 设置插件包名和 Web Controller 标记             |
| 鉴权控制      | `skipAuth` 会设置公开 metadata                 |
| path 校验     | path 不能包含 `/` 和 `:`                       |

### ExtensionConsoleController

导入：

```ts
import { ExtensionConsoleController } from "@buildingai/core/decorators";
```

用途：声明插件控制台 API Controller。

```ts
import { BaseController } from "@buildingai/base";
import { ExtensionConsoleController } from "@buildingai/core/decorators";
import { Playground } from "@buildingai/decorators/playground.decorator";
import { type UserPlayground } from "@buildingai/db";
import { UUIDValidationPipe } from "@buildingai/pipe/param-validate.pipe";
import { Body, Delete, Get, Param, Post, Query } from "@nestjs/common";

@ExtensionConsoleController("article", "文章管理")
export class ArticleController extends BaseController {
    constructor(private readonly articleService: ArticleService) {
        super();
    }

    @Post()
    async create(@Body() dto: CreateArticleDto, @Playground() user: UserPlayground) {
        return this.articleService.createArticle(dto, user.id);
    }

    @Get()
    async findAll(@Query() query: QueryArticleDto) {
        return this.articleService.list(query);
    }

    @Delete(":id")
    async remove(@Param("id", UUIDValidationPipe) id: string) {
        await this.articleService.delete(id);
        return { success: true };
    }
}
```

可用参数：

| 写法                                                                           | 说明                   |
| ------------------------------------------------------------------------------ | ---------------------- |
| `@ExtensionConsoleController("article", "文章管理")`                           | 设置 path 和权限组名称 |
| `@ExtensionConsoleController({ path: "article" }, "文章管理")`                 | 对象写法               |
| `@ExtensionConsoleController({ path: "article", skipAuth: true }, "文章管理")` | 跳过鉴权               |

内置能力：

| 能力            | 说明                                                                   |
| --------------- | ---------------------------------------------------------------------- |
| 插件路由前缀    | 默认 `/{extensionIdentifier}/consoleapi/{path}`                        |
| 环境前缀        | 如果存在 `VITE_APP_CONSOLE_API_PREFIX`，使用该前缀                     |
| 插件 metadata   | 设置插件包名和 Console Controller 标记                                 |
| 权限组 metadata | 自动设置 `code: "${extensionIdentifier}@${path}"` 和 `name: groupName` |
| path 校验       | path 不能包含 `/` 和 `:`                                               |

## 插件 Entity

### ExtensionEntity

导入：

```ts
import { ExtensionEntity } from "@buildingai/core/decorators";
```

用途：声明插件实体并自动归入插件 schema。

```ts
import { ExtensionEntity } from "@buildingai/core/decorators";
import {
    Column,
    CreateDateColumn,
    PrimaryGeneratedColumn,
    UpdateDateColumn,
} from "@buildingai/db/typeorm";

@ExtensionEntity()
export class Article {
    @PrimaryGeneratedColumn("uuid")
    id: string;

    @Column({ length: 200, comment: "文章标题" })
    title: string;

    @Column({ type: "text", nullable: true, comment: "摘要" })
    summary?: string;

    @CreateDateColumn()
    createdAt: Date;

    @UpdateDateColumn()
    updatedAt: Date;
}
```

可用写法：

| 写法                                    | 说明                         |
| --------------------------------------- | ---------------------------- |
| `@ExtensionEntity()`                    | 表名默认用类名转 snake_case  |
| `@ExtensionEntity("article")`           | 指定表名                     |
| `@ExtensionEntity({ name: "article" })` | 使用 TypeORM `EntityOptions` |

内置能力：

| 能力           | 说明                             |
| -------------- | -------------------------------- |
| 插件 schema    | 根据插件安装目录推导 schema      |
| 表名处理       | 支持默认表名和自定义表名         |
| TypeORM Entity | 内部应用 TypeORM `Entity` 装饰器 |

## 基础 Controller

### BaseController

导入：

```ts
import { BaseController } from "@buildingai/base";
```

可用能力：

| 能力               | 说明                                   |
| ------------------ | -------------------------------------- |
| `logger`           | 自动创建 Nest Logger，context 为子类名 |
| `paginationResult` | 组装标准分页响应                       |

```ts
import { BaseController } from "@buildingai/base";

export class ArticleController extends BaseController {
    async list(dto: QueryArticleDto) {
        this.logger.log("query article list");
        return this.articleService.list(dto);
    }
}
```

分页响应格式：

```ts
{
    items: T[];
    total: number;
    page: number;
    pageSize: number;
    totalPages: number;
}
```

## 基础 Service

### BaseService

导入：

```ts
import { BaseService } from "@buildingai/base";
```

用法：

```ts
import { BaseService } from "@buildingai/base";
import { InjectRepository } from "@buildingai/db/@nestjs/typeorm";
import { Repository } from "@buildingai/db/typeorm";
import { HttpErrorFactory } from "@buildingai/errors";
import { Injectable } from "@nestjs/common";

import { Article } from "../../../db/entities/article.entity";
import { CreateArticleDto, QueryArticleDto } from "../dto";

@Injectable()
export class ArticleService extends BaseService<Article> {
    constructor(
        @InjectRepository(Article)
        private readonly articleRepository: Repository<Article>,
    ) {
        super(articleRepository);
    }

    async createArticle(dto: CreateArticleDto) {
        return this.create(dto);
    }

    async list(query: QueryArticleDto) {
        const where = query.title ? this.ilike("title", query.title) : {};

        return this.paginate(query, {
            where,
            order: { createdAt: "DESC" },
        });
    }

    async publish(id: string) {
        const article = await this.findOneById(id);

        if (!article) {
            throw HttpErrorFactory.notFound("文章不存在");
        }

        return this.updateById(id, { status: "published" });
    }
}
```

可用方法：

| 方法                     | 说明              |
| ------------------------ | ----------------- |
| `create`                 | 创建单条记录      |
| `createMany`             | 批量创建          |
| `updateById`             | 按 id 更新        |
| `update`                 | 按条件更新        |
| `findOneById`            | 按 id 查询        |
| `findOne`                | 按条件查询单条    |
| `findAll`                | 查询列表          |
| `count`                  | 计数              |
| `delete`                 | 删除              |
| `deleteMany`             | 批量删除          |
| `restore`                | 恢复软删除记录    |
| `paginate`               | Repository 分页   |
| `paginateQueryBuilder`   | QueryBuilder 分页 |
| `createTransaction`      | 创建事务          |
| `withTransaction`        | 在事务中执行      |
| `withRetry`              | 重试执行          |
| `applyLockToFindOptions` | 查询锁配置        |

查询辅助方法：

| 方法                            | 说明                            |
| ------------------------------- | ------------------------------- |
| `ilike(field, value)`           | PostgreSQL 不区分大小写模糊搜索 |
| `textSearch(field, value)`      | PostgreSQL 全文搜索             |
| `jsonQuery(field, path, value)` | JSON 字段查询                   |
| `arrayContains(field, values)`  | 数组包含查询                    |

字段过滤：

| 选项            | 说明                                   |
| --------------- | -------------------------------------- |
| `includeFields` | 只返回指定字段，支持 `author.nickname` |
| `excludeFields` | 排除指定字段，支持嵌套字段             |

```ts
return this.findOneById(id, {
    relations: ["author"],
    includeFields: ["id", "title", "author.id", "author.nickname"] as const,
});
```

## DTO 和 Pipe

### PaginationDto

导入：

```ts
import { PaginationDto } from "@buildingai/dto";
```

可用字段：

| 字段       | 默认值 | 说明     |
| ---------- | ------ | -------- |
| `page`     | `1`    | 当前页   |
| `pageSize` | `15`   | 每页数量 |

```ts
import { PaginationDto } from "@buildingai/dto";
import { IsOptional, IsString, IsUUID } from "class-validator";

export class QueryArticleDto extends PaginationDto {
    @IsOptional()
    @IsString()
    title?: string;

    @IsOptional()
    @IsUUID()
    categoryId?: string;
}
```

### UUIDValidationPipe

导入：

```ts
import { UUIDValidationPipe } from "@buildingai/pipe/param-validate.pipe";
```

```ts
import { Get, Param } from "@nestjs/common";

@Get(":id")
async findOne(@Param("id", UUIDValidationPipe) id: string) {
    return this.articleService.findOneById(id);
}
```

## 通用装饰器

### Public

导入：

```ts
import { Public } from "@buildingai/decorators/public.decorator";
```

用途：标记公开接口。

```ts
@Get("published")
@Public()
async getPublished() {
    return this.articleService.getPublishedArticles();
}
```

### Playground

导入：

```ts
import { Playground } from "@buildingai/decorators/playground.decorator";
```

用途：获取当前登录用户上下文。

```ts
import { type UserPlayground } from "@buildingai/db";

@Post()
async create(@Body() dto: CreateArticleDto, @Playground() user: UserPlayground) {
    return this.articleService.createArticle(dto, user.id);
}
```

支持获取指定属性：

```ts
async create(@Playground("id") userId: string) {
    return userId;
}
```

### BuildFileUrl

导入：

```ts
import { BuildFileUrl } from "@buildingai/decorators";
```

用途：把文件字段转换为完整可访问 URL。

```ts
@Get(":id")
@BuildFileUrl(["cover", "author.avatar", "items.*.thumbnail"])
async findOne(@Param("id", UUIDValidationPipe) id: string) {
    return this.articleService.findOneById(id);
}
```

可用写法：

| 写法                                 | 说明         |
| ------------------------------------ | ------------ |
| `"cover"`                            | 普通字段     |
| `"author.avatar"`                    | 嵌套字段     |
| `"items.*.thumbnail"`                | 数组通配字段 |
| `{ field: "images", isArray: true }` | 数组字段     |

### SkipTransform

导入：

```ts
import { SkipTransform } from "@buildingai/decorators";
```

用途：跳过统一响应转换。

```ts
@Get("raw")
@SkipTransform()
async raw() {
    return "plain text";
}
```

## 错误封装

### HttpErrorFactory

导入：

```ts
import { HttpErrorFactory } from "@buildingai/errors";
```

可用方法：

| 方法                    | 说明               |
| ----------------------- | ------------------ |
| `badRequest(message)`   | 参数或业务状态错误 |
| `unauthorized(message)` | 未登录或认证失败   |
| `forbidden(message)`    | 无权限             |
| `notFound(message)`     | 资源不存在         |
| `internal(message)`     | 服务内部错误       |

```ts
if (!article) {
    throw HttpErrorFactory.notFound("文章不存在");
}

if (!category) {
    throw HttpErrorFactory.badRequest("栏目不存在");
}
```

其他导出：

```ts
import { ApplicationError, HttpError, HttpStatus } from "@buildingai/errors";
```

## 数据库工具

### TypeORM Module 和 Repository

导入：

```ts
import { InjectRepository, TypeOrmModule } from "@buildingai/db/@nestjs/typeorm";
import { Repository } from "@buildingai/db/typeorm";
```

模块注册：

```ts
import { TypeOrmModule } from "@buildingai/db/@nestjs/typeorm";
import { Module } from "@nestjs/common";

import { Article } from "../../db/entities/article.entity";
import { ArticleController } from "./controllers/console/article.controller";
import { ArticleWebController } from "./controllers/web/article.web.controller";
import { ArticleService } from "./services/article.service";

@Module({
    imports: [TypeOrmModule.forFeature([Article])],
    controllers: [ArticleController, ArticleWebController],
    providers: [ArticleService],
    exports: [ArticleService],
})
export class ArticleModule {}
```

Repository 注入：

```ts
constructor(
    @InjectRepository(Article)
    private readonly articleRepository: Repository<Article>,
) {}
```

### TypeORM 装饰器和操作符

导入：

```ts
import {
    Column,
    CreateDateColumn,
    In,
    Like,
    ManyToOne,
    PrimaryGeneratedColumn,
    UpdateDateColumn,
} from "@buildingai/db/typeorm";
```

常用导出：

| 类型                                   | 说明               |
| -------------------------------------- | ------------------ |
| `Column`                               | 字段               |
| `PrimaryGeneratedColumn`               | 主键               |
| `CreateDateColumn`                     | 创建时间           |
| `UpdateDateColumn`                     | 更新时间           |
| `ManyToOne`、`OneToMany`、`JoinColumn` | 关联               |
| `In`、`Like`、`ILike`、`Raw`           | 查询操作符         |
| `Repository`                           | TypeORM Repository |
| `FindOptionsWhere`                     | 查询条件类型       |

### 平台实体

导入：

```ts
import { User } from "@buildingai/db/entities";
```

可用于插件实体关联平台用户：

```ts
import { User } from "@buildingai/db/entities";
import { JoinColumn, ManyToOne } from "@buildingai/db/typeorm";

@ManyToOne(() => User, { nullable: true })
@JoinColumn({ name: "authorId" })
author?: User;
```

## 插件 SDK

### PublicUserService

导入：

```ts
import { PublicUserService } from "@buildingai/extension-sdk";
```

用途：读取平台用户信息。

```ts
constructor(private readonly userService: PublicUserService) {}

async attachAuthor(authorId: string) {
    const author = await this.userService.findUserById(authorId);

    if (!author) {
        throw HttpErrorFactory.notFound("作者不存在");
    }

    return author;
}
```

### AiPublicModule 和 PublicAiModelService

导入：

```ts
import { AiPublicModule, PublicAiModelService } from "@buildingai/extension-sdk";
```

模块使用：

```ts
import { AiPublicModule } from "@buildingai/extension-sdk";
import { Module } from "@nestjs/common";

@Module({
    imports: [AiPublicModule],
    providers: [GenerateService],
    exports: [GenerateService],
})
export class GenerateModule {}
```

Service 注入：

```ts
import { PublicAiModelService } from "@buildingai/extension-sdk";

constructor(private readonly aiModelService: PublicAiModelService) {}
```

### ExtensionBillingModule 和 ExtensionBillingService

导入：

```ts
import { ExtensionBillingModule, ExtensionBillingService } from "@buildingai/extension-sdk";
```

模块使用：

```ts
import { ExtensionBillingModule } from "@buildingai/extension-sdk";
import { Module } from "@nestjs/common";

@Module({
    imports: [ExtensionBillingModule],
    providers: [GenerateService],
    exports: [GenerateService],
})
export class GenerateModule {}
```

Service 注入：

```ts
import { ExtensionBillingService } from "@buildingai/extension-sdk";

constructor(private readonly billingService: ExtensionBillingService) {}
```

导出类型：

```ts
import type { ExtensionPowerDeductionOptions } from "@buildingai/extension-sdk";
```

## 工具函数

### buildWhere

导入：

```ts
import { buildWhere } from "@buildingai/utils";
```

用途：过滤掉 `undefined` 条件，构造 TypeORM where。

```ts
import { Like } from "@buildingai/db/typeorm";
import { buildWhere } from "@buildingai/utils";

const where = buildWhere<Article>({
    title: query.title ? Like(`%${query.title}%`) : undefined,
    categoryId: query.categoryId,
    status: query.status,
});

return this.paginate(query, { where });
```

### 类型转换工具

导入：

```ts
import {
    asArray,
    asBoolean,
    asDate,
    asDefined,
    asError,
    asJson,
    asNumber,
    asObject,
    asString,
} from "@buildingai/utils";
```

| 工具        | 说明      |
| ----------- | --------- |
| `asArray`   | 转数组    |
| `asString`  | 转字符串  |
| `asNumber`  | 转数字    |
| `asBoolean` | 转布尔    |
| `asObject`  | 转对象    |
| `asDate`    | 转日期    |
| `asJson`    | 解析 JSON |
| `asDefined` | 过滤空值  |
| `asError`   | 转 Error  |

### 路径工具

导入：

```ts
import { joinPaths, joinRouterPaths, validatePath } from "@buildingai/utils";
```

| 工具              | 说明         |
| ----------------- | ------------ |
| `joinPaths`       | 拼接普通路径 |
| `joinRouterPaths` | 拼接路由路径 |
| `validatePath`    | 校验路径片段 |

### 插件标识工具

导入：

```ts
import { parseExtensionIdentifier, parsePackageName } from "@buildingai/utils";
```

| 工具                       | 说明         |
| -------------------------- | ------------ |
| `parseExtensionIdentifier` | 解析插件标识 |
| `parsePackageName`         | 解析包名     |

### 安全和版本工具

导入：

```ts
import { checkVersionCompatibility, decryptValue, maskSensitiveValue } from "@buildingai/utils";
```

| 工具                        | 说明         |
| --------------------------- | ------------ |
| `maskSensitiveValue`        | 脱敏字符串   |
| `decryptValue`              | 解密值       |
| `checkVersionCompatibility` | 检查版本兼容 |

### Promise 和流工具

导入：

```ts
import { createResolvablePromise, DelayedPromise, StreamUtils } from "@buildingai/utils";
```

| 工具                      | 说明                                 |
| ------------------------- | ------------------------------------ |
| `createResolvablePromise` | 创建可外部 resolve/reject 的 Promise |
| `DelayedPromise`          | 延迟 Promise 工具                    |
| `StreamUtils`             | 流处理工具                           |

### 消息内容工具

导入：

```ts
import { extractFilesFromMessageContent, extractTextFromMessageContent } from "@buildingai/utils";
```

| 工具                             | 说明                 |
| -------------------------------- | -------------------- |
| `extractTextFromMessageContent`  | 从消息内容中提取文本 |
| `extractFilesFromMessageContent` | 从消息内容中提取文件 |

## Nest 常用导入

插件后端 Controller 和 Module 可以直接使用 Nest 标准装饰器：

```ts
import { Body, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common";
import { Injectable, Module } from "@nestjs/common";
```

常用装饰器：

| 装饰器        | 场景         |
| ------------- | ------------ |
| `@Module`     | 声明模块     |
| `@Injectable` | 声明 Service |
| `@Get`        | GET 接口     |
| `@Post`       | POST 接口    |
| `@Patch`      | PATCH 接口   |
| `@Delete`     | DELETE 接口  |
| `@Body`       | 请求体       |
| `@Query`      | 查询参数     |
| `@Param`      | 路径参数     |

