For AI agents: the complete documentation index is available at https://docs.halo.run/llms.txt, the full documentation bundle is available at https://docs.halo.run/llms-full.txt, and this page is available as Markdown at https://docs.halo.run/developer-guide/plugin/api-reference/server/custom-endpoints.md.

自定义 API

自动生成的自定义模型 CRUD API 不能满足聚合查询或业务动作时,可以定义响应式自定义 API。简单资源读写仍应优先使用自定义模型 API

选择 API Group

自定义 API 的 group 决定主要使用范围和 OpenAPI 分组:

使用范围group 示例
Consoleconsole.api.my-plugin.halo.run
UC 个人中心uc.api.my-plugin.halo.run
主题或其他公开调用方api.my-plugin.halo.run

自定义 API 仍需配置最小权限。使用公开 group 不会自动允许匿名访问,权限规则见角色模板

使用 CustomEndpoint

CustomEndpoint 从 Halo 2.0.0 开始提供,为 RouterFunction 自动添加 group 和 version 前缀:

import static org.springframework.http.MediaType.APPLICATION_JSON;

import java.util.Map;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.extension.GroupVersion;

@Component
public class PersonEndpoint implements CustomEndpoint {

    @Override
    public RouterFunction<ServerResponse> endpoint() {
        return RouterFunctions.route()
            .GET("/persons/{name}", this::getPerson)
            .build();
    }

    private Mono<ServerResponse> getPerson(ServerRequest request) {
        return ServerResponse.ok()
            .contentType(APPLICATION_JSON)
            .bodyValue(Map.of("name", request.pathVariable("name")));
    }

    @Override
    public GroupVersion groupVersion() {
        return new GroupVersion("console.api.my-plugin.halo.run", "v1alpha1");
    }
}

最终路径为 /apis/console.api.my-plugin.halo.run/v1alpha1/persons/{name}。保持资源型 API 路径不超过角色模板支持的层级;复杂筛选使用查询参数。

使用带注解的控制器

ApiVersion 从 Halo 2.0.0 开始提供。需要注解风格时,可以使用 @RestController,并通过 @ApiVersion 添加 Halo API group 和 version 前缀。缺少该注解时,控制器仍可能按原始 @RequestMapping 路径注册,因此不能用省略注解的方式禁用 API:

@ApiVersion("console.api.my-plugin.halo.run/v1alpha1")
@RequestMapping("/persons")
@RestController
public class PersonController {

    @GetMapping("/{name}")
    public Mono<Map<String, String>> get(@PathVariable String name) {
        return Mono.just(Map.of("name", name));
    }
}

Halo 基于 WebFlux;控制器不能返回阻塞调用。无法替换的阻塞工作应按响应式服务端开发隔离。

处理列表参数

需要支持分页、排序和选择器时,可以在 Halo 2.5.0 及以上版本继承 SortableRequest,复用 Halo 的查询参数解析:

public class PersonQuery extends SortableRequest {

    public PersonQuery(ServerWebExchange exchange) {
        super(exchange);
    }

    public String getKeyword() {
        return queryParams.getFirst("keyword");
    }

    @Override
    public ListOptions toListOptions() {
        var builder = ListOptions.builder(super.toListOptions());
        if (StringUtils.isNotBlank(getKeyword())) {
            builder.andQuery(contains("spec.name", getKeyword()));
        }
        return builder.build();
    }
}

spec.name 必须已经注册索引。查询与分页调用见 ReactiveExtensionClient

校验请求体

自定义 API 的输入属于信任边界。使用 Jakarta Bean Validation 或等价的显式校验,不要只依赖 OpenAPI 描述:

public class PersonParam {
    @NotBlank
    @Size(max = 100)
    private String name;

    @Min(0)
    @Max(150)
    private int age;
}

WebFlux Functional Endpoint 不会仅凭字段上的约束注解自动执行校验。先在插件应用上下文中提供 Validator:

@Configuration
public class PluginConfig {

    @Bean
    public LocalValidatorFactoryBean validator() {
        return new LocalValidatorFactoryBean();
    }
}

然后在读取请求体后显式调用:

private final Validator validator;

private Mono<ServerResponse> createPerson(ServerRequest request) {
    return request.bodyToMono(PersonParam.class)
        .doOnNext(this::validate)
        .flatMap(param -> ServerResponse.ok().bodyValue(param));
}

private void validate(PersonParam param) {
    var errors = new BeanPropertyBindingResult(param, "person");
    validator.validate(param, errors);
    if (errors.hasErrors()) {
        throw new ServerWebInputException(errors.toString());
    }
}

这里的 Validatororg.springframework.validation.Validator。校验失败时返回可定位的 4xx 错误,不能把无效输入写入自定义模型。涉及当前用户语言时,在请求范围内设置 Locale,并在结束后恢复。

生成 OpenAPI 文档

需要让 Functional Endpoint 出现在 Swagger 和生成的 API Client 中时,使用 SpringdocRouteBuilder 描述 operationId、tag、参数和响应。operationId 应稳定且唯一,tag 建议包含资源 Kind、版本和作用域。

OpenAPI 分组与 API group 对应。生成客户端的步骤见 DevTools。不要手动修改生成的 api-clientapi-docs

源码参考