Java 复杂enum显示在swagger-ui 以及line break列值解决方案

本文介绍如何在Java API中使用SpringDoc Swagger展示自定义枚举,并解决枚举值显示格式问题,包括加入OpenApiCustomizer和调整Swagger UI样式。

目录

背景:

前提条件:

添加eunm

enum列值问题


背景:

在java中我们需要用到大量的自定义enum class, 而如果想要把这enum 作为一个API的有可能返回值(List of Values),那我们有时候就需要把这些enum class加入到swagger ui 中,同时把enum列出来。

前提条件:

enum 加入到springdoc swagger Schema, 自定义enum并加入注解

@Schema(enumAsRef = true)

example:

@AllArgsConstructor
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
@Schema(enumAsRef = true,
        name = "ResponseHeaderMessages",
        description = "List of Values of Standard API Response Body Header",
        title = "List of Values of Standard API Response Body Header"
)
public enum VOUC_API_MESSAGE implements Serializable

添加eunm

如果enum并非API返回值,enum就不能被swagger加入到Schema或Models中,这时候就需要自定义OpenApiCustomiser。在OpenAPI Config 加入以下自定义Bean 用作添加全局可能API返回和自定义enum

@Bean
    public OpenApiCustomiser customerGlobalOpenApiCustomizer() {
        return openApi -> {
            if(openApi != null) {
                openApi.getPaths().values().forEach(pathItem -> pathItem.readOperations().forEach(operation -> {
                    ApiResponses apiResponses = operation.getResponses();
                    Content content = new Content();
                    MediaType mediaType = new MediaType();
                    mediaType.setSchema(openApi.getComponents().getSchemas().get(VoucResponseMessage.class.getSimpleName()));
                    content.addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE, mediaType);
                    ApiResponse apiResponse = new ApiResponse().description("Common Unauthorized Error").content(content);
                    ApiResponse notFoundApiResponse = new ApiResponse().description("Common Not Found Error").content(content);
                    apiResponses.addApiResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()), apiResponse);
                    apiResponses.addApiResponse(String.valueOf(HttpStatus.NOT_FOUND.value()), notFoundApiResponse);
                }));
//添加enum Schema                
openApi.getComponents().getSchemas().putAll(ModelConverters.getInstance().readAll(VOUC_API_MESSAGE.class));
            }
        };
    }

enum列值问题

一般enum列值swagger只会用到enum value name。

如果enum比较复杂,而且作为json object 返回到API中,这时候我们需要加入enum的toString方法。

- @JsonValue 将被swagger ModelResolver 用到

    @Override
    @JsonValue
    public String toString() {
        return "{" +
                "code:" + code +
                ", strCode:'" + strCode + '\'' +
                ", httpStatus:" + httpStatus +
                ", message:'" + message + '\'' +
                "}";
    }

经过以上步骤,我们就可以把自定义的enum加入到springdoc 或 swagger中。但是这时候就会发现swagger显示enum值就会全部显示在同一行,或者根本就没有分行。

enum line break解决方法

1. 在enum toString 方法中加入 line break 符号

    @Override
    @JsonValue
    public String toString() {
        return "\r\n{" + //Line break 符号
                "code:" + code +
                ", strCode:'" + strCode + '\'' +
                ", httpStatus:" + httpStatus +
                ", message:'" + message + '\'' +
                "}";
    }

2. 引用自定义的swagger-ui.css 并修改一下css (white-space: pre; 加入到 prop-enum)

.swagger-ui .prop-enum {
    display: block;
    # add below line
    white-space: pre;
}

这样我们的enum列值就会完美显示出来

第一个文件:# save_model.py import joblib, os from sklearn.ensemble import RandomForestClassifier import numpy as np # 生成模拟数据并训练 X = np.random.rand(300, 3) # plan_ratio, actual_ratio, remain_hours y = np.random.choice([0, 1, 2], 300) # 0滞后 1正常 2超前 clf = RandomForestClassifier(n_estimators=200, random_state=42).fit(X, y) # 创建 models 目录并保存 os.makedirs("models", exist_ok=True) joblib.dump(clf, "models/progress_model.pkl") print("✅ progress_model.pkl 已生成") 能正常运行 第二个文件: # 导入必要的库和模块 from fastapi import FastAPI from fastapi.openapi.docs import get_swagger_ui_html from fastapi.exceptions import HTTPException, RequestValidationError from fastapi.responses import JSONResponse from pydantic import BaseModel, Field, field_validator, ValidationInfo import joblib import numpy as np from datetime import datetime, timedelta from typing import Dict, List, Optional # ==================== FastAPI 实例配置 ==================== # 创建FastAPI应用实例,并配置元数据信息 app = FastAPI( title="工程进度智能分析系统", # API标题 description="基于建材用量分析的工程进度状态评估服务", # API描述 version="2.0.0", # API版本 contact={ # 联系方式 "name": "技术支持", "email": "support@engineering.ai" }, license_info={ # 许可证信息 "name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html", }, docs_url="/docs", # Swagger文档路径 redoc_url=None, # 禁用Redoc文档 ) # ==================== 数据模型定义 ==================== class MaterialInput(BaseModel): """单种建材输入参数模型""" total_planned: float = Field(..., gt=0, example=1000, description="计划总量(必须大于0)") total_used: float = Field(..., ge=0, example=300, description="已用量(不能小于0)") start_date: str = Field(..., example="2023-01-01", description="计划开始日期(YYYY-MM-DD)") end_date: str = Field(..., example="2023-12-31", description="计划结束日期(YYYY-MM-DD)") current_date: str = Field(default_factory=lambda: datetime.now().strftime('%Y-%m-%d'), description="分析基准日期(默认当天)") @field_validator('end_date') @classmethod def validate_dates(cls, v: str, info: ValidationInfo) -> str: """验证日期有效性:结束日期必须晚于开始日期""" try: start_date_str = info.data.get('start_date') if not start_date_str: raise ValueError("需要提供start_date") start = datetime.strptime(start_date_str, '%Y-%m-%d') end = datetime.strptime(v, '%Y-%m-%d') if end <= start: raise ValueError("结束日期必须晚于开始日期") return v except ValueError as e: raise ValueError(f"日期验证失败: {str(e)}") @field_validator('total_used') @classmethod def validate_usage(cls, v: float, info: ValidationInfo) -> float: """验证用量有效性:已用量不能超过计划总量""" if 'total_planned' in info.data and v > info.data['total_planned']: raise ValueError("已用量不能超过计划总量") return v class ProgressRequest(BaseModel): """进度分析请求参数模型""" materials: Dict[str, MaterialInput] = Field( ..., example={ "水泥": { "total_planned": 1000, "total_used": 300, "start_date": "2023-01-01", "end_date": "2023-12-31" } }, description="建材名称到参数的映射" ) method: str = Field( default="weighted", enum=["strict", "weighted", "average"], description="""进度计算方法: strict-所有建材达标才算正常 | weighted-按重要度加权计算 | average-简单平均""" ) weights: Optional[Dict[str, float]] = Field( None, description="当method=weighted时需要的权重配置(自动归一化)" ) class MaterialProgress(BaseModel): """单种建材进度分析结果模型""" progress_rate: float = Field(..., example=0.3, description="完成进度比例(0~1)") planned_rate: float = Field(..., example=0.5, description="计划进度比例(0~1)") status: str = Field(..., example="滞后", description="进度状态:滞后|正常|超前") days_remaining: int = Field(..., example=100, description="预计剩余天数") completion_date: str = Field(..., example="2023-12-15", description="预计完成日期") class ProgressResponse(BaseModel): """进度分析响应结果模型""" overall_status: str = Field(..., example="滞后", description="整体进度状态") materials: Dict[str, MaterialProgress] = Field( ..., description="每种建材的详细分析结果" ) metrics: dict = Field( ..., example={ "avg_progress": 0.45, "min_progress": 0.3, "max_progress": 0.6 }, description="综合统计指标" ) recommendations: List[str] = Field( ..., example=["建议增加钢筋供应量", "混凝土浇筑进度滞后需加快"], description="改进建议表" ) # ==================== 进度分析核心逻辑 ==================== def calculate_material_progress(material: MaterialInput) -> MaterialProgress: """ 计算单种建材进度 参数: material: 建材输入参数 返回: MaterialProgress: 包含进度计算结果的对象 """ # 日期解析 start_date = datetime.strptime(material.start_date, '%Y-%m-%d') end_date = datetime.strptime(material.end_date, '%Y-%m-%d') current_date = datetime.strptime(material.current_date, '%Y-%m-%d') # 计算计划进度 total_days = (end_date - start_date).days # 总计划天数 elapsed_days = (current_date - start_date).days # 已过去天数 # 计算计划进度比例 if elapsed_days <= 0: planned_progress = 0.0 # 未开始 elif elapsed_days >= total_days: planned_progress = 1.0 # 已超期 else: planned_progress = elapsed_days / total_days # 计划进度比例 # 计算实际进度比例 actual_progress = material.total_used / material.total_planned # 预计完成时间计算 if actual_progress <= 0: remaining_days = float('inf') # 无穷大表示无法预测 completion_date = "无法预测(零消耗)" else: # 计算使用速率和剩余天数 usage_rate = material.total_used / max(1, elapsed_days) remaining_amount = material.total_planned - material.total_used remaining_days = remaining_amount / usage_rate if usage_rate > 0 else float('inf') completion_date = (current_date + timedelta(days=remaining_days)).strftime('%Y-%m-%d') # 判断进度状态(允许10%的缓冲区间) if actual_progress < planned_progress * 0.9: status = "滞后" elif actual_progress > planned_progress * 1.1: status = "超前" else: status = "正常" # 返回计算结果对象 return MaterialProgress( progress_rate=round(actual_progress, 4), planned_rate=round(planned_progress, 4), status=status, days_remaining=int(round(remaining_days)), completion_date=completion_date ) def calculate_overall_progress( materials: Dict[str, MaterialProgress], method: str = "weighted", weights: Optional[Dict[str, float]] = None ) -> dict: """ 计算整体进度状态 参数: materials: 各建材的进度计算结果字典 method: 计算方法 (strict|weighted|average) weights: 权重配置(仅weighted方法需要) 返回: dict: 包含整体状态和指标的字典 """ # 提取所有进度和计划 progress_values = [m.progress_rate for m in materials.values()] planned_values = [m.planned_rate for m in materials.values()] # 计算综合指标 metrics = { "avg_progress": round(sum(progress_values) / len(progress_values), 4), # 平均进度 "min_progress": round(min(progress_values), 4), # 最小进度 "max_progress": round(max(progress_values), 4), # 最大进度 "avg_planned": round(sum(planned_values) / len(planned_values), 4) # 平均计划进度 } # 根据不同的计算方法确定整体状态 if method == "strict": # 严格模式:所有材料达标才算正常 status = "正常" for m in materials.values(): if m.status == "滞后": status = "滞后" break elif m.status == "超前": status = "超前" elif method == "weighted": # 加权计算模式 if not weights: weights = {name: 1.0 for name in materials} # 默认权重为1 # 权重归一化处理 total_weight = sum(weights.get(name, 0) for name in materials) if total_weight <= 0: total_weight = 1.0 # 计算加权分数 weighted_sum = 0 for name, progress in materials.items(): weight = weights.get(name, 0) / total_weight # 滞后=-1, 正常=0, 超前=1 score = -1 if progress.status == "滞后" else (1 if progress.status == "超前" else 0) weighted_sum += score * weight # 根据加权分数确定状态 if weighted_sum < -0.3: status = "滞后" elif weighted_sum > 0.3: status = "超前" else: status = "正常" else: # average # 简单平均模式 if metrics['avg_progress'] < metrics['avg_planned'] * 0.95: status = "滞后" elif metrics['avg_progress'] > metrics['avg_planned'] * 1.05: status = "超前" else: status = "正常" return { "overall_status": status, "metrics": metrics } def generate_recommendations(response_data: dict) -> List[str]: """生成改进建议""" recommendations = [] if response_data["overall_status"] == "滞后": recommendations.append("建议召开进度协调会,分析滞后原因") for material, data in response_data["materials"].items(): if data["status"] == "滞后": rec = f"{material}:当前进度{data['progress_rate'] * 100:.1f}%," rec += f"计划应达{data['planned_rate'] * 100:.1f}%。" rec += f"预计完成日期比计划晚{data['days_remaining']}天" recommendations.append(rec) return recommendations # ==================== API接口 ==================== @app.post( "/analyze/progress", response_model=ProgressResponse, summary="建材进度综合分析", description="基于多类建材的消耗情况评估整体工程进度", tags=["分析服务"] ) async def analyze_progress(request: ProgressRequest): """ 工程进度分析主接口 参数: request: 包含建材数据和计算方法的请求对象 返回: ProgressResponse: 包含详细分析结果的响应对象 """ try: # 计算每种建材的进度 material_results = { name: calculate_material_progress(data) for name, data in request.materials.items() } # 计算整体进度 overall = calculate_overall_progress( material_results, request.method, request.weights ) # 生成建议 recommendations = generate_recommendations({ "overall_status": overall["overall_status"], "materials": material_results }) # 返回响应对象 return ProgressResponse( overall_status=overall["overall_status"], materials=material_results, metrics=overall["metrics"], recommendations=recommendations ) except Exception as e: # 异常处理 raise HTTPException(status_code=400, detail=str(e)) # ==================== 健康检查 ==================== @app.get("/", summary="服务健康检查", tags=["系统管理"]) async def health_check(): """服务健康检查端点""" return { "status": "running", "version": app.version, "service": "material-progress-analysis" } # ==================== 错误处理 ==================== @app.exception_handler(RequestValidationError) async def validation_exception_handler(request, exc): """处理请求参数验证错误""" errors = [] for error in exc.errors(): field = "->".join(str(loc) for loc in error["loc"]) # 拼接错误字段路径 errors.append(f"{field} {error['msg']}") # 格式化错误信息 # 返回标准化的错误响应 return JSONResponse( status_code=422, content={ "detail": "参数验证失败", "errors": errors, "suggestion": "请参考API文档检查参数格式" }, ) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8080) # 修改端口号为8080 运行之后报错:D:\anaconda\envs\jh\python.exe C:\Users\31477\main.py Traceback (most recent call last): File "C:\Users\31477\main.py", line 6, in <module> from pydantic import BaseModel, Field, field_validator, ValidationInfo ImportError: cannot import name 'field_validator' from 'pydantic' (D:\anaconda\envs\jh\lib\site-packages\pydantic\__init__.cp39-win_amd64.pyd) Process finished with exit code 1 第三个文件:import subprocess import webbrowser import time import os import sys # 获取当前脚本的绝对路径 current_dir = os.path.dirname(os.path.abspath(__file__)) # 1. 启动 FastAPI 服务(后台) cmd = [sys.executable, "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"] proc = subprocess.Popen(cmd, cwd=current_dir) # 2. 等 2 秒后打开浏览器 time.sleep(2) webbrowser.open("http://127.0.0.1:8000/docs") # 3. 保持脚本运行(按 Ctrl+C 结束) try: proc.wait() except KeyboardInterrupt: proc.terminate() 运行之后报错 :D:\anaconda\envs\jh\python.exe C:\Users\31477\lunch2.py Traceback (most recent call last): File "D:\anaconda\envs\jh\lib\runpy.py", line 197, in _run_module_as_main return _run_code(code, main_globals, None, File "D:\anaconda\envs\jh\lib\runpy.py", line 87, in _run_code exec(code, run_globals) File "D:\anaconda\envs\jh\lib\site-packages\uvicorn\__main__.py", line 4, in <module> uvicorn.main() File "D:\anaconda\envs\jh\lib\site-packages\click\core.py", line 1161, in __call__ return self.main(*args, **kwargs) File "D:\anaconda\envs\jh\lib\site-packages\click\core.py", line 1082, in main rv = self.invoke(ctx) File "D:\anaconda\envs\jh\lib\site-packages\click\core.py", line 1443, in invoke return ctx.invoke(self.callback, **ctx.params) File "D:\anaconda\envs\jh\lib\site-packages\click\core.py", line 788, in invoke return __callback(*args, **kwargs) File "D:\anaconda\envs\jh\lib\site-packages\uvicorn\main.py", line 413, in main run( File "D:\anaconda\envs\jh\lib\site-packages\uvicorn\main.py", line 580, in run server.run() File "D:\anaconda\envs\jh\lib\site-packages\uvicorn\server.py", line 67, in run return asyncio.run(self.serve(sockets=sockets)) File "D:\anaconda\envs\jh\lib\asyncio\runners.py", line 44, in run return loop.run_until_complete(main) File "D:\anaconda\envs\jh\lib\asyncio\base_events.py", line 647, in run_until_complete return future.result() File "D:\anaconda\envs\jh\lib\site-packages\uvicorn\server.py", line 71, in serve await self._serve(sockets) File "D:\anaconda\envs\jh\lib\site-packages\uvicorn\server.py", line 78, in _serve config.load() File "D:\anaconda\envs\jh\lib\site-packages\uvicorn\config.py", line 436, in load self.loaded_app = import_from_string(self.app) File "D:\anaconda\envs\jh\lib\site-packages\uvicorn\importer.py", line 19, in import_from_string module = importlib.import_module(module_str) File "D:\anaconda\envs\jh\lib\importlib\__init__.py", line 127, in import_module return _bootstrap._gcd_import(name[level:], package, level) File "<frozen importlib._bootstrap>", line 1030, in _gcd_import File "<frozen importlib._bootstrap>", line 1007, in _find_and_load File "<frozen importlib._bootstrap>", line 986, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 680, in _load_unlocked File "<frozen importlib._bootstrap_external>", line 850, in exec_module File "<frozen importlib._bootstrap>", line 228, in _call_with_frames_removed File "C:\Users\31477\main.py", line 6, in <module> from pydantic import BaseModel, Field, field_validator, ValidationInfo ImportError: cannot import name 'field_validator' from 'pydantic' (D:\anaconda\envs\jh\lib\site-packages\pydantic\__init__.cp39-win_amd64.pyd) Process finished with exit code 0 并且弹出网页但无法访问。 怎么办
08-11
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值