java-api-endpoint

Installation
SKILL.md

API 端点开发

在 Spring Boot 分层架构项目中添加 RESTful API 端点。

前置: 遵守 java-architecture-guide 中的分层原则。

RESTful 规范

路径格式

/api/v{version}/{resources}
  • 资源名: 复数名词, kebab-case
  • 示例: /api/v1/alert-policies, /api/v1/video-sources

标准 CRUD 映射

操作 方法 路径 返回
创建 POST /api/v1/policies ApiResponse<IdResp>
更新 PUT /api/v1/policies/{id} ApiResponse<Void>
删除 DELETE /api/v1/policies/{id} ApiResponse<Void>
详情 GET /api/v1/policies/{id} ApiResponse<DetailDTO>
分页列表 GET /api/v1/policies?page=1&size=20 ApiResponse<PageResult<Card>>
游标分页 GET /api/v1/policies/stream?cursor=&size=20 ApiResponse<CursorPageResult<Card>>
批量操作 POST /api/v1/policies/batch/{action} ApiResponse<BatchResp>

Controller 模式

类声明

@Slf4j
@RestController
@RequestMapping(path = "/api/v1/policies", produces = MediaType.APPLICATION_JSON_VALUE)
@Tag(name = "预警策略", description = "预警策略管理接口")
@RequiredArgsConstructor
public class PolicyController {
    private final PolicyFacade facade;  // 只注入 Facade
}

必需注解:

  • @Tag(name, description) — OpenAPI 分组
  • @RequestMapping(path, produces=JSON) — 统一 JSON 响应
  • @RequiredArgsConstructor — 构造器注入

统一响应

所有方法返回 ApiResponse<T>:

ApiResponse.ok()              // 无数据成功
ApiResponse.ok(data)          // 带数据成功
ApiResponse.error(code, msg)  // 错误

参数校验

// Request Body 校验
@PostMapping
public ApiResponse<IdResp> create(@Valid @RequestBody PolicyCreateReq req)

// Path Variable 校验 (类上需加 @Validated)
@Validated
@RestController
public class PolicyController {
    @DeleteMapping("/{id}")
    public ApiResponse<Void> delete(
        @NotNull(message = "ID不能为空")
        @Positive(message = "ID必须为正数")
        @PathVariable Long id)
}

5 种端点模板

1. 分页查询 (偏移分页)

适用于小数据量,前端需要页码跳转。

@Operation(summary = "分页查询策略列表")
@GetMapping
public ApiResponse<PageResult<PolicyCard>> getPage(
        @Parameter(description = "页码", example = "1")
        @RequestParam(defaultValue = "1") int page,
        @Parameter(description = "每页大小", example = "20")
        @RequestParam(defaultValue = "20") int size,
        @Parameter(description = "策略名称")
        @RequestParam(required = false) String name,
        @Parameter(description = "状态")
        @RequestParam(required = false) String status) {
    return ApiResponse.ok(facade.getPage(page, size, name, status));
}

2. 游标分页 (大数据量)

适用于大数据量,避免深分页性能问题。

@Operation(summary = "游标分页查询")
@GetMapping("/stream")
public ApiResponse<CursorPageResult<PolicyCard>> getStreamPage(
        @Parameter(description = "游标(上页最后一条ID)")
        @RequestParam(required = false) Long cursor,
        @Parameter(description = "每页大小")
        @RequestParam(defaultValue = "20") int size,
        @Parameter(description = "策略名称")
        @RequestParam(required = false) String name) {
    if (cursor != null && cursor < 0L) {
        cursor = null;
    }
    return ApiResponse.ok(facade.getStreamPage(cursor, size, name));
}

Service 层游标分页实现要点:

LambdaQueryWrapper<Entity> wrapper = new LambdaQueryWrapper<>();
if (cursor != null) {
    wrapper.lt(Entity::getId, cursor);  // ID < cursor
}
wrapper.orderByDesc(Entity::getId)
       .last("LIMIT " + (size + 1));   // 多查一条判断 hasMore

List<Entity> list = mapper.selectList(wrapper);
boolean hasMore = list.size() > size;
if (hasMore) list = list.subList(0, size);

3. 详情查询

@Operation(summary = "查询策略详情")
@GetMapping("/{id}")
public ApiResponse<PolicyDetail> getDetail(
        @Parameter(description = "策略ID", example = "1")
        @PathVariable Long id) {
    return ApiResponse.ok(facade.getDetail(id));
}

4. 创建 / 更新 / 删除

@Operation(summary = "创建策略")
@PostMapping
public ApiResponse<IdResp> create(@Valid @RequestBody PolicyCreateReq req) {
    Long id = facade.create(req);
    return ApiResponse.ok(new IdResp(id));
}

@Operation(summary = "更新策略")
@PutMapping("/{id}")
public ApiResponse<Void> update(
        @Parameter(description = "策略ID") @PathVariable Long id,
        @Valid @RequestBody PolicyUpdateReq req) {
    facade.update(id, req);
    return ApiResponse.ok();
}

@Operation(summary = "删除策略")
@DeleteMapping("/{id}")
public ApiResponse<Void> delete(
        @Parameter(description = "策略ID") @PathVariable Long id) {
    facade.delete(id);
    return ApiResponse.ok();
}

5. 批量操作

@Operation(summary = "批量启动任务")
@PostMapping("/batch/start")
public ApiResponse<TaskBatchResp> batchStart(
        @Valid @RequestBody TaskBatchReq req) {
    return ApiResponse.ok(facade.batchStart(req));
}

Facade 层批量操作模式 (部分成功):

public TaskBatchResp batchStart(TaskBatchReq req) {
    TaskBatchResp resp = new TaskBatchResp();
    for (Long taskId : req.getTaskIds()) {
        try {
            startTask(taskId);
            resp.addSuccess(taskId);
        } catch (Exception e) {
            resp.addFailure(taskId, e.getMessage());
        }
    }
    return resp;
}

Facade 编排模式

写操作: 验证 → 执行 → 返回

@Transactional(rollbackFor = Exception.class)
public Long create(PolicyCreateReq req) {
    // 1. 验证关联数据有效性
    validateRelatedData(req);
    // 2. 调用 Service 执行
    Long id = policyService.create(req);
    // 3. 创建关联数据
    createTimePlans(id, req.getTimePlans());
    return id;
}

读操作: 查询 → 批量关联 → 丰富结果

public PageResult<PolicyCard> getPage(int page, int size, String name, String status) {
    // 1. 查询主数据
    PageResult<PolicyCard> result = policyService.getPage(page, size, name, status);
    // 2. 提取 ID 批量查询关联数据
    List<Long> policyIds = result.getRecords().stream()
            .map(PolicyCard::getId).toList();
    Map<Long, List<TimePlan>> planMap = timePlanService.batchGetByPolicyIds(policyIds);
    // 3. 丰富结果
    result.getRecords().forEach(card ->
            card.setTimePlans(planMap.getOrDefault(card.getId(), List.of())));
    return result;
}

OpenAPI 注解速查

注解 位置 用途
@Tag(name, description) Controller 类 API 分组
@Operation(summary, description) 方法 操作说明
@Parameter(description, example) 参数 参数说明
@Schema(description, example) DTO 字段 字段说明

启用/禁用端点模式

@Operation(summary = "切换启用状态")
@PatchMapping("/{id}/enabled")
public ApiResponse<Void> switchEnabled(
        @PathVariable Long id,
        @RequestParam boolean enabled) {
    facade.switchEnabled(id, enabled);
    return ApiResponse.ok();
}

Facade 层:

@Transactional(rollbackFor = Exception.class)
public void switchEnabled(Long id, boolean enabled) {
    if (enabled) {
        // 启用前检查依赖是否就绪
        validateDependenciesReady(id);
    } else {
        // 禁用前检查是否被其他模块使用
        checkNotInUse(id);
    }
    policyService.switchEnabled(id, enabled);
}
Related skills

More from jianyun8023/my-skills

Installs
1
GitHub Stars
1
First Seen
Mar 29, 2026