feat: add utoipa path annotations to all API handlers and wire OpenAPI spec

- Add #[utoipa::path] annotations to all 70+ handler functions across
  auth, config, workflow, and message modules
- Add IntoParams/ToSchema derives to Pagination, PaginatedResponse, ApiResponse
  in erp-core, and MessageQuery/TemplateQuery in erp-message
- Collect all module paths into OpenAPI spec via AuthApiDoc, ConfigApiDoc,
  WorkflowApiDoc, MessageApiDoc structs in erp-server main.rs
- Update openapi_spec handler to merge all module specs
- The /docs/openapi.json endpoint now returns complete API documentation
  with all endpoints, request/response schemas, and security requirements
This commit is contained in:
iven
2026-04-15 01:23:27 +08:00
parent ee65b6e3c9
commit e44d6063be
21 changed files with 1165 additions and 22 deletions

View File

@@ -10,6 +10,17 @@ use crate::auth_state::AuthState;
use crate::dto::{LoginReq, LoginResp, RefreshReq};
use crate::service::auth_service::{AuthService, JwtConfig};
#[utoipa::path(
post,
path = "/api/v1/auth/login",
request_body = LoginReq,
responses(
(status = 200, description = "登录成功", body = ApiResponse<LoginResp>),
(status = 400, description = "请求参数错误"),
(status = 401, description = "用户名或密码错误"),
),
tag = "认证"
)]
/// POST /api/v1/auth/login
///
/// Authenticates a user with username and password, returning access and refresh tokens.
@@ -48,6 +59,16 @@ where
Ok(Json(ApiResponse::ok(resp)))
}
#[utoipa::path(
post,
path = "/api/v1/auth/refresh",
request_body = RefreshReq,
responses(
(status = 200, description = "刷新成功", body = ApiResponse<LoginResp>),
(status = 401, description = "刷新令牌无效或已过期"),
),
tag = "认证"
)]
/// POST /api/v1/auth/refresh
///
/// Validates an existing refresh token, revokes it (rotation), and issues
@@ -71,6 +92,16 @@ where
Ok(Json(ApiResponse::ok(resp)))
}
#[utoipa::path(
post,
path = "/api/v1/auth/logout",
responses(
(status = 200, description = "已成功登出"),
(status = 401, description = "未授权"),
),
security(("bearer_auth" = [])),
tag = "认证"
)]
/// POST /api/v1/auth/logout
///
/// Revokes all refresh tokens for the authenticated user, effectively

View File

@@ -19,6 +19,17 @@ use erp_core::rbac::require_permission;
// --- Organization handlers ---
#[utoipa::path(
get,
path = "/api/v1/organizations",
responses(
(status = 200, description = "成功", body = ApiResponse<Vec<OrganizationResp>>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "组织管理"
)]
/// GET /api/v1/organizations
///
/// List all organizations within the current tenant as a nested tree.
@@ -37,6 +48,18 @@ where
Ok(Json(ApiResponse::ok(tree)))
}
#[utoipa::path(
post,
path = "/api/v1/organizations",
request_body = CreateOrganizationReq,
responses(
(status = 200, description = "创建成功", body = ApiResponse<OrganizationResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "组织管理"
)]
/// POST /api/v1/organizations
///
/// Create a new organization within the current tenant.
@@ -67,6 +90,20 @@ where
Ok(Json(ApiResponse::ok(org)))
}
#[utoipa::path(
put,
path = "/api/v1/organizations/{id}",
params(("id" = Uuid, Path, description = "组织ID")),
request_body = UpdateOrganizationReq,
responses(
(status = 200, description = "更新成功", body = ApiResponse<OrganizationResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "组织不存在"),
),
security(("bearer_auth" = [])),
tag = "组织管理"
)]
/// PUT /api/v1/organizations/{id}
///
/// Update editable organization fields (name, code, sort_order).
@@ -87,6 +124,19 @@ where
Ok(Json(ApiResponse::ok(org)))
}
#[utoipa::path(
delete,
path = "/api/v1/organizations/{id}",
params(("id" = Uuid, Path, description = "组织ID")),
responses(
(status = 200, description = "组织已删除"),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "组织不存在"),
),
security(("bearer_auth" = [])),
tag = "组织管理"
)]
/// DELETE /api/v1/organizations/{id}
///
/// Soft-delete an organization by ID.
@@ -113,6 +163,18 @@ where
// --- Department handlers ---
#[utoipa::path(
get,
path = "/api/v1/organizations/{org_id}/departments",
params(("org_id" = Uuid, Path, description = "组织ID")),
responses(
(status = 200, description = "成功", body = ApiResponse<Vec<DepartmentResp>>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "组织管理"
)]
/// GET /api/v1/organizations/{org_id}/departments
///
/// List all departments for an organization as a nested tree.
@@ -132,6 +194,19 @@ where
Ok(Json(ApiResponse::ok(tree)))
}
#[utoipa::path(
post,
path = "/api/v1/organizations/{org_id}/departments",
params(("org_id" = Uuid, Path, description = "组织ID")),
request_body = CreateDepartmentReq,
responses(
(status = 200, description = "创建成功", body = ApiResponse<DepartmentResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "组织管理"
)]
/// POST /api/v1/organizations/{org_id}/departments
///
/// Create a new department under the specified organization.
@@ -164,6 +239,20 @@ where
Ok(Json(ApiResponse::ok(dept)))
}
#[utoipa::path(
put,
path = "/api/v1/departments/{id}",
params(("id" = Uuid, Path, description = "部门ID")),
request_body = UpdateDepartmentReq,
responses(
(status = 200, description = "更新成功", body = ApiResponse<DepartmentResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "部门不存在"),
),
security(("bearer_auth" = [])),
tag = "组织管理"
)]
/// PUT /api/v1/departments/{id}
///
/// Update editable department fields (name, code, manager_id, sort_order).
@@ -184,6 +273,19 @@ where
Ok(Json(ApiResponse::ok(dept)))
}
#[utoipa::path(
delete,
path = "/api/v1/departments/{id}",
params(("id" = Uuid, Path, description = "部门ID")),
responses(
(status = 200, description = "部门已删除"),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "部门不存在"),
),
security(("bearer_auth" = [])),
tag = "组织管理"
)]
/// DELETE /api/v1/departments/{id}
///
/// Soft-delete a department by ID.
@@ -210,6 +312,18 @@ where
// --- Position handlers ---
#[utoipa::path(
get,
path = "/api/v1/departments/{dept_id}/positions",
params(("dept_id" = Uuid, Path, description = "部门ID")),
responses(
(status = 200, description = "成功", body = ApiResponse<Vec<PositionResp>>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "组织管理"
)]
/// GET /api/v1/departments/{dept_id}/positions
///
/// List all positions for a department.
@@ -229,6 +343,19 @@ where
Ok(Json(ApiResponse::ok(positions)))
}
#[utoipa::path(
post,
path = "/api/v1/departments/{dept_id}/positions",
params(("dept_id" = Uuid, Path, description = "部门ID")),
request_body = CreatePositionReq,
responses(
(status = 200, description = "创建成功", body = ApiResponse<PositionResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "组织管理"
)]
/// POST /api/v1/departments/{dept_id}/positions
///
/// Create a new position under the specified department.
@@ -261,6 +388,20 @@ where
Ok(Json(ApiResponse::ok(pos)))
}
#[utoipa::path(
put,
path = "/api/v1/positions/{id}",
params(("id" = Uuid, Path, description = "岗位ID")),
request_body = UpdatePositionReq,
responses(
(status = 200, description = "更新成功", body = ApiResponse<PositionResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "岗位不存在"),
),
security(("bearer_auth" = [])),
tag = "组织管理"
)]
/// PUT /api/v1/positions/{id}
///
/// Update editable position fields (name, code, level, sort_order).
@@ -281,6 +422,19 @@ where
Ok(Json(ApiResponse::ok(pos)))
}
#[utoipa::path(
delete,
path = "/api/v1/positions/{id}",
params(("id" = Uuid, Path, description = "岗位ID")),
responses(
(status = 200, description = "岗位已删除"),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "岗位不存在"),
),
security(("bearer_auth" = [])),
tag = "组织管理"
)]
/// DELETE /api/v1/positions/{id}
///
/// Soft-delete a position by ID.

View File

@@ -13,6 +13,18 @@ use crate::service::permission_service::PermissionService;
use crate::service::role_service::RoleService;
use erp_core::rbac::require_permission;
#[utoipa::path(
get,
path = "/api/v1/roles",
params(Pagination),
responses(
(status = 200, description = "成功", body = ApiResponse<PaginatedResponse<RoleResp>>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "角色管理"
)]
/// GET /api/v1/roles
///
/// List roles within the current tenant with pagination.
@@ -43,6 +55,18 @@ where
})))
}
#[utoipa::path(
post,
path = "/api/v1/roles",
request_body = CreateRoleReq,
responses(
(status = 200, description = "创建成功", body = ApiResponse<RoleResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "角色管理"
)]
/// POST /api/v1/roles
///
/// Create a new role within the current tenant.
@@ -75,6 +99,19 @@ where
Ok(Json(ApiResponse::ok(role)))
}
#[utoipa::path(
get,
path = "/api/v1/roles/{id}",
params(("id" = Uuid, Path, description = "角色ID")),
responses(
(status = 200, description = "成功", body = ApiResponse<RoleResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "角色不存在"),
),
security(("bearer_auth" = [])),
tag = "角色管理"
)]
/// GET /api/v1/roles/:id
///
/// Fetch a single role by ID within the current tenant.
@@ -94,6 +131,20 @@ where
Ok(Json(ApiResponse::ok(role)))
}
#[utoipa::path(
put,
path = "/api/v1/roles/{id}",
params(("id" = Uuid, Path, description = "角色ID")),
request_body = UpdateRoleReq,
responses(
(status = 200, description = "更新成功", body = ApiResponse<RoleResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "角色不存在"),
),
security(("bearer_auth" = [])),
tag = "角色管理"
)]
/// PUT /api/v1/roles/:id
///
/// Update editable role fields (name, description).
@@ -123,6 +174,19 @@ where
Ok(Json(ApiResponse::ok(role)))
}
#[utoipa::path(
delete,
path = "/api/v1/roles/{id}",
params(("id" = Uuid, Path, description = "角色ID")),
responses(
(status = 200, description = "角色已删除"),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "角色不存在"),
),
security(("bearer_auth" = [])),
tag = "角色管理"
)]
/// DELETE /api/v1/roles/:id
///
/// Soft-delete a role by ID within the current tenant.
@@ -148,6 +212,20 @@ where
}))
}
#[utoipa::path(
post,
path = "/api/v1/roles/{id}/permissions",
params(("id" = Uuid, Path, description = "角色ID")),
request_body = AssignPermissionsReq,
responses(
(status = 200, description = "权限分配成功"),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "角色不存在"),
),
security(("bearer_auth" = [])),
tag = "角色管理"
)]
/// POST /api/v1/roles/:id/permissions
///
/// Replace all permission assignments for a role.
@@ -180,6 +258,19 @@ where
}))
}
#[utoipa::path(
get,
path = "/api/v1/roles/{id}/permissions",
params(("id" = Uuid, Path, description = "角色ID")),
responses(
(status = 200, description = "成功", body = ApiResponse<Vec<PermissionResp>>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "角色不存在"),
),
security(("bearer_auth" = [])),
tag = "角色管理"
)]
/// GET /api/v1/roles/:id/permissions
///
/// Fetch all permissions assigned to a role.
@@ -199,6 +290,17 @@ where
Ok(Json(ApiResponse::ok(perms)))
}
#[utoipa::path(
get,
path = "/api/v1/permissions",
responses(
(status = 200, description = "成功", body = ApiResponse<Vec<PermissionResp>>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "权限管理"
)]
/// GET /api/v1/permissions
///
/// List all permissions within the current tenant.

View File

@@ -2,6 +2,7 @@ use axum::Extension;
use axum::extract::{FromRef, Path, Query, State};
use axum::response::Json;
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use validator::Validate;
use erp_core::error::AppError;
@@ -14,7 +15,7 @@ use crate::service::user_service::UserService;
use erp_core::rbac::require_permission;
/// Query parameters for user list endpoint.
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, IntoParams)]
pub struct UserListParams {
pub page: Option<u64>,
pub page_size: Option<u64>,
@@ -22,6 +23,18 @@ pub struct UserListParams {
pub search: Option<String>,
}
#[utoipa::path(
get,
path = "/api/v1/users",
params(UserListParams),
responses(
(status = 200, description = "成功", body = ApiResponse<PaginatedResponse<UserResp>>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "用户管理"
)]
/// GET /api/v1/users
///
/// List users within the current tenant with pagination and optional search.
@@ -62,6 +75,18 @@ where
})))
}
#[utoipa::path(
post,
path = "/api/v1/users",
request_body = CreateUserReq,
responses(
(status = 200, description = "创建成功", body = ApiResponse<UserResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "用户管理"
)]
/// POST /api/v1/users
///
/// Create a new user within the current tenant.
@@ -92,6 +117,19 @@ where
Ok(Json(ApiResponse::ok(user)))
}
#[utoipa::path(
get,
path = "/api/v1/users/{id}",
params(("id" = Uuid, Path, description = "用户ID")),
responses(
(status = 200, description = "成功", body = ApiResponse<UserResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "用户不存在"),
),
security(("bearer_auth" = [])),
tag = "用户管理"
)]
/// GET /api/v1/users/:id
///
/// Fetch a single user by ID within the current tenant.
@@ -111,6 +149,20 @@ where
Ok(Json(ApiResponse::ok(user)))
}
#[utoipa::path(
put,
path = "/api/v1/users/{id}",
params(("id" = Uuid, Path, description = "用户ID")),
request_body = UpdateUserReq,
responses(
(status = 200, description = "更新成功", body = ApiResponse<UserResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "用户不存在"),
),
security(("bearer_auth" = [])),
tag = "用户管理"
)]
/// PUT /api/v1/users/:id
///
/// Update editable user fields.
@@ -131,6 +183,19 @@ where
Ok(Json(ApiResponse::ok(user)))
}
#[utoipa::path(
delete,
path = "/api/v1/users/{id}",
params(("id" = Uuid, Path, description = "用户ID")),
responses(
(status = 200, description = "用户已删除"),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "用户不存在"),
),
security(("bearer_auth" = [])),
tag = "用户管理"
)]
/// DELETE /api/v1/users/:id
///
/// Soft-delete a user by ID within the current tenant.
@@ -156,17 +221,31 @@ where
}
/// Assign roles request body.
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, ToSchema)]
pub struct AssignRolesReq {
pub role_ids: Vec<Uuid>,
}
/// Assign roles response.
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, ToSchema)]
pub struct AssignRolesResp {
pub roles: Vec<RoleResp>,
}
#[utoipa::path(
post,
path = "/api/v1/users/{id}/roles",
params(("id" = Uuid, Path, description = "用户ID")),
request_body = AssignRolesReq,
responses(
(status = 200, description = "角色分配成功", body = ApiResponse<AssignRolesResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "用户不存在"),
),
security(("bearer_auth" = [])),
tag = "用户管理"
)]
/// POST /api/v1/users/:id/roles
///
/// Replace all role assignments for a user within the current tenant.