基于swagger插件的方式推送接口文档至torna

news2024/9/27 7:25:06

目录

  • 一、前言
  • 二、登录torna
  • 三、创建/选择空间
  • 四、创建/选择项目
  • 五、创建/选择应用
  • 六、获取应用的token
  • 七、服务推送
    • 7.1 引入maven依赖
    • 7.2 test下面按照如下方式新建文件

一、前言

Torna作为一款企业级文档管理系统,支持了很多种接口文档的推送方式。官方比较推荐的一种方式,就是使用smart-doc插件推送,该插件需要完善接口代码中的javadoc,相对来说,代码规范性要求较高。
使用方式如下:
接口文档管理解决方案调研及Torna+Smart-doc的使用

这里,由于某些老项目,javadoc并不规范,而且某些接口连swagger注解都没有。所以,在这里提供了一种基于swagger插件的方式,利用main方法推送文档至torna的方式。

二、登录torna

在这里插入图片描述

三、创建/选择空间

这里空间可以配置为某个具体的环境,例如:开发环境、测试环境。
在这里插入图片描述

四、创建/选择项目

在这里插入图片描述

五、创建/选择应用

在这里插入图片描述

六、获取应用的token

在这里插入图片描述

七、服务推送

说明:

由于默认的swagger插件只支持扫描带有@Api的Controller以及只带有@ApiOperation的接口方法,这里兼容了无swagger注解的接口推送。

7.1 引入maven依赖

  <dependency>
      <groupId>cn.torna</groupId>
      <artifactId>swagger-plugin</artifactId>
      <version>1.2.14</version>
      <scope>test</scope>
  </dependency>

7.2 test下面按照如下方式新建文件

在这里插入图片描述

  • torna.json
{
  // 开启推送
  "enable": true,
  // 扫描package,多个用;隔开
  "basePackage": "com.product",
  // 推送URL,IP端口对应Torna服务器
  "url": "http://test.xxx.com:7700/torna/api",
  // 模块token,复制应用的token
  "token": "xxxxxxxxxxxxxxxxxxxxxxxxxx",
  "debugEnv": "test,https://test.xxx.com/product",
  // 推送人
  "author": "author",
  // 打开调试:true/false
  "debug": true,
  // 是否替换文档,true:替换,false:不替换(追加)。默认:true
  "isReplace": false
}
  • DocPushTest.java
import cn.torna.swaggerplugin.TmlySwaggerPlugin;

public class DocPushTest {
    public static void main(String[] args) {
        TmlySwaggerPlugin.pushDoc();
    }
}
  • TmlySwaggerPlugin.java
package cn.torna.swaggerplugin;

import cn.torna.swaggerplugin.bean.TornaConfig;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.StreamUtils;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;

public class TmlySwaggerPlugin {


    /**
     * 推送文档,前提:把<code>torna.json</code>文件复制到resources下
     */
    public static void pushDoc() {
        pushDoc("torna.json");
    }

    /**
     * 推送swagger文档
     *
     * @param configFile 配置文件
     */
    public static void pushDoc(String configFile) {
        pushDoc(configFile, TmlySwaggerPluginService.class);
    }


    public static void pushDoc(String configFile, Class<? extends SwaggerPluginService> swaggerPluginServiceClazz) {
        ClassPathResource classPathResource = new ClassPathResource(configFile);
        if (!classPathResource.exists()) {
            throw new IllegalArgumentException("找不到文件:" + configFile + ",请确保resources下有torna.json");
        }
        System.out.println("加载Torna配置文件:" + configFile);
        try {
            InputStream inputStream = classPathResource.getInputStream();
            String json = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
            JSONObject jsonObject = JSON.parseObject(json);
            TornaConfig tornaConfig = jsonObject.toJavaObject(TornaConfig.class);
            Constructor<? extends SwaggerPluginService> constructor = swaggerPluginServiceClazz.getConstructor(TornaConfig.class);
            SwaggerPluginService swaggerPluginService = constructor.newInstance(tornaConfig);
            swaggerPluginService.pushDoc();
        } catch (IOException | InstantiationException | IllegalAccessException | NoSuchMethodException |
                 InvocationTargetException e) {
            e.printStackTrace();
            throw new RuntimeException("推送文档出错", e);
        }
    }
}
  • TmlySwaggerPluginService.java
package cn.torna.swaggerplugin;

import cn.torna.sdk.param.DocItem;
import cn.torna.swaggerplugin.bean.Booleans;
import cn.torna.swaggerplugin.bean.ControllerInfo;
import cn.torna.swaggerplugin.bean.PluginConstants;
import cn.torna.swaggerplugin.bean.TornaConfig;
import cn.torna.swaggerplugin.builder.MvcRequestInfoBuilder;
import cn.torna.swaggerplugin.builder.RequestInfoBuilder;
import cn.torna.swaggerplugin.exception.HiddenException;
import cn.torna.swaggerplugin.exception.IgnoreException;
import cn.torna.swaggerplugin.util.ClassUtil;
import cn.torna.swaggerplugin.util.PluginUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.annotations.ApiIgnore;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.stream.Collectors;

public class TmlySwaggerPluginService extends SwaggerPluginService {
    private final TornaConfig tornaConfig;

    public TmlySwaggerPluginService(TornaConfig tornaConfig) {
        super(tornaConfig);
        this.tornaConfig = tornaConfig;
    }

    public void pushDoc() {
        if (!tornaConfig.getEnable()) {
            return;
        }
        String basePackage = tornaConfig.getBasePackage();
        if (StringUtils.isEmpty(basePackage)) {
            throw new IllegalArgumentException("basePackage can not empty.");
        }
        this.doPush();
        this.pushCode();
    }

    protected void doPush() {
        String packageConfig = tornaConfig.getBasePackage();
        String[] pkgs = packageConfig.split(";");
        Set<Class<?>> classes = new HashSet<>();
        for (String basePackage : pkgs) {
//            Set<Class<?>> clazzs = ClassUtil.getClasses(basePackage, Api.class);
            // 把带有RestController的控制层抽取出来
            Set<Class<?>> clazzs = ClassUtil.getClasses(basePackage, RestController.class);
            classes.addAll(clazzs);
        }
        Map<ControllerInfo, List<DocItem>> controllerDocMap = new HashMap<>(32);
        for (Class<?> clazz : classes) {
            ControllerInfo controllerInfo;
            try {
                controllerInfo = buildControllerInfo(clazz);
            } catch (HiddenException | IgnoreException e) {
                System.out.println(e.getMessage());
                continue;
            }
            List<DocItem> docItems = controllerDocMap.computeIfAbsent(controllerInfo, k -> new ArrayList<>());
            ReflectionUtils.doWithMethods(clazz, method -> {
                try {
                    DocItem apiInfo = this.buildDocItem(new MvcRequestInfoBuilder(method, tornaConfig));
                    docItems.add(apiInfo);
                } catch (HiddenException | IgnoreException e) {
                    System.out.println(e.getMessage());
                } catch (Exception e) {
                    System.out.printf("Create doc error, method:%s%n", method);
                    throw new RuntimeException(e.getMessage(), e);
                }
            }, this::match);
        }
        List<DocItem> docItems = mergeSameFolder(controllerDocMap);
        this.push(docItems);
    }


    private ControllerInfo buildControllerInfo(Class<?> controllerClass) throws HiddenException, IgnoreException {
        Api api = AnnotationUtils.findAnnotation(controllerClass, Api.class);
        ApiIgnore apiIgnore = AnnotationUtils.findAnnotation(controllerClass, ApiIgnore.class);
        if (api != null && api.hidden()) {
            throw new HiddenException("Hidden doc(@Api.hidden=true):" + api.value());
        }
        if (apiIgnore != null) {
            throw new IgnoreException("Ignore doc(@ApiIgnore):" + controllerClass.getName());
        }
        String name, description;
        int position = 0;
        if (api == null) {
            name = controllerClass.getSimpleName();
            description = "";
        } else {
            name = api.value();
            if (StringUtils.isEmpty(name) && api.tags().length > 0) {
                name = api.tags()[0];
            }
            description = api.description();
            position = api.position();
        }
        ControllerInfo controllerInfo = new ControllerInfo();
        controllerInfo.setName(name);
        controllerInfo.setDescription(description);
        controllerInfo.setPosition(position);
        return controllerInfo;
    }


    /**
     * 合并控制层文档
     * 按照控制层类的顺序及名称(@Api为value,否则类的getSimpleName),合并为一个有序的文档数组
     *
     * @param controllerDocMap 控制层->文档集合
     * @return
     */
    private List<DocItem> mergeSameFolder(Map<ControllerInfo, List<DocItem>> controllerDocMap) {
        // key:文件夹,value:文档
        Map<String, List<DocItem>> folderDocMap = new HashMap<>();
        controllerDocMap.forEach((key, value) -> {
            List<DocItem> docItems = folderDocMap.computeIfAbsent(key.getName(), k -> new ArrayList<>());
            docItems.addAll(value);
        });
        List<ControllerInfo> controllerInfoList = controllerDocMap.keySet()
                .stream()
                .sorted(Comparator.comparing(ControllerInfo::getPosition))
                .collect(Collectors.toList());

        List<DocItem> folders = new ArrayList<>(controllerDocMap.size());
        for (Map.Entry<String, List<DocItem>> entry : folderDocMap.entrySet()) {
            String name = entry.getKey();
            ControllerInfo info = controllerInfoList
                    .stream()
                    .filter(controllerInfo -> name.equals(controllerInfo.getName()))
                    .findFirst()
                    .orElse(null);
            if (info == null) {
                continue;
            }
            DocItem docItem = new DocItem();
            docItem.setName(name);
            docItem.setDefinition(info.getDescription());
            docItem.setOrderIndex(info.getPosition());
            docItem.setIsFolder(Booleans.TRUE);
            List<DocItem> items = entry.getValue();
            items.sort(Comparator.comparing(DocItem::getOrderIndex));
            docItem.setItems(items);
            folders.add(docItem);
        }
        return folders;
    }

    protected DocItem buildDocItem(RequestInfoBuilder requestInfoBuilder) throws HiddenException, IgnoreException {
        Method method = requestInfoBuilder.getMethod();
        ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
        ApiIgnore apiIgnore = method.getAnnotation(ApiIgnore.class);
        if (apiOperation != null && apiOperation.hidden()) {
            throw new HiddenException("Hidden API(@ApiOperation.hidden=true):" + apiOperation.value());
        }
        if (apiIgnore != null) {
            throw new IgnoreException("Ignore API(@ApiIgnore):" + apiOperation.value());
        }
        return this.doBuildDocItem(requestInfoBuilder);
    }


    /**
     * 兼容方法名上@ApiOperation为空的情况
     *
     * @param requestInfoBuilder
     * @return
     */
    protected DocItem doBuildDocItem(RequestInfoBuilder requestInfoBuilder) {
        ApiOperation apiOperation = requestInfoBuilder.getApiOperation();
        Method method = requestInfoBuilder.getMethod();
        DocItem docItem = new DocItem();
        String httpMethod = getHttpMethod(requestInfoBuilder);
        docItem.setAuthor(apiOperation != null ? buildAuthor(apiOperation) : "");
        docItem.setName(apiOperation != null ? apiOperation.value() : method.getName());
        docItem.setDescription(apiOperation != null ? apiOperation.notes() : "");
        docItem.setOrderIndex(apiOperation != null ? buildOrder(apiOperation, method) : 0);
        docItem.setUrl(requestInfoBuilder.buildUrl());
        String contentType = buildContentType(requestInfoBuilder);
        docItem.setHttpMethod(httpMethod);
        docItem.setContentType(contentType);
        docItem.setIsFolder(PluginConstants.FALSE);
        docItem.setPathParams(buildPathParams(method));
        docItem.setHeaderParams(buildHeaderParams(method));
        docItem.setQueryParams(buildQueryParams(method, httpMethod));
        TmlyDocParamWrapper reqWrapper = new TmlyDocParamWrapper();
        BeanUtils.copyProperties(buildRequestParams(method, httpMethod), reqWrapper);
        TmlyDocParamWrapper respWrapper = new TmlyDocParamWrapper();
        BeanUtils.copyProperties(buildResponseParams(method), respWrapper);
        docItem.setRequestParams(reqWrapper.getData());
        docItem.setResponseParams(respWrapper.getData());
        docItem.setIsRequestArray(reqWrapper.getIsArray());
        docItem.setRequestArrayType(reqWrapper.getArrayType());
        docItem.setIsResponseArray(respWrapper.getIsArray());
        docItem.setResponseArrayType(respWrapper.getArrayType());
        docItem.setErrorCodeParams(apiOperation != null ? buildErrorCodes(apiOperation) : new ArrayList<>(0));
        return docItem;
    }

    private String getHttpMethod(RequestInfoBuilder requestInfoBuilder) {
        ApiOperation apiOperation = requestInfoBuilder.getApiOperation();
        Method method = requestInfoBuilder.getMethod();
        if (apiOperation != null && StringUtils.hasText(apiOperation.httpMethod())) {
            return apiOperation.httpMethod();
        }
        RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
        if (requestMapping != null) {
            RequestMethod[] methods = requestMapping.method();
            if (methods.length == 0) {
                return this.tornaConfig.getMethodWhenMulti();
            } else {
                return methods[0].name();
            }
        }
        return tornaConfig.getDefaultHttpMethod();
    }

    private String buildContentType(RequestInfoBuilder requestInfoBuilder) {
        ApiOperation apiOperation = requestInfoBuilder.getApiOperation();
        Method method = requestInfoBuilder.getMethod();
        if (apiOperation != null && StringUtils.hasText(apiOperation.consumes())) {
            return apiOperation.consumes();
        }
        String[] consumeArr = getConsumes(method);
        if (consumeArr != null && consumeArr.length > 0) {
            return consumeArr[0];
        }
        Parameter[] methodParameters = method.getParameters();
        if (methodParameters.length == 0) {
            return "";
        }
        for (Parameter methodParameter : methodParameters) {
            RequestBody requestBody = methodParameter.getAnnotation(RequestBody.class);
            if (requestBody != null) {
                return MediaType.APPLICATION_JSON_VALUE;
            }
            if (PluginUtil.isFileParameter(methodParameter)) {
                return MediaType.MULTIPART_FORM_DATA_VALUE;
            }
        }
        return getTornaConfig().getGlobalContentType();
    }

    private String[] getConsumes(Method method) {
        RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
        if (requestMapping != null) {
            return requestMapping.consumes();
        }
        return null;
    }

    public boolean match(Method method) {
        List<String> scanApis = this.tornaConfig.getScanApis();
        if (CollectionUtils.isEmpty(scanApis)) {
//            return method.getAnnotation(ApiOperation.class) != null;
            return AnnotatedElementUtils.hasAnnotation(method, RequestMapping.class);
        }
        for (String scanApi : scanApis) {
            String methodName = method.toString();
            if (methodName.contains(scanApi)) {
                return true;
            }
        }
        return false;
    }



    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    private static class TmlyDocParamWrapper<T> {
        /**
         * 是否数组
         */
        private Byte isArray;

        /**
         * 数组元素类型
         */
        private String arrayType;

        private List<T> data;
    }

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1920174.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于EMQX+Flask+InfluxDB+Grafana打造多协议物联网云平台:MQTT/HTTP设备接入与数据可视化流程(附代码示例)

摘要: 本文深入浅出地介绍了物联网、云平台、MQTT、HTTP、数据可视化等核心概念&#xff0c;并结合 EMQX、Flask、InfluxDB、Grafana 等主流工具&#xff0c;手把手教你搭建一个支持多协议的物联网云平台。文章结构清晰&#xff0c;图文并茂&#xff0c;代码翔实易懂&#xff0…

Linux 入门教程 by 程序员鱼皮

本文作者&#xff1a;程序员鱼皮 免费编程学习 - 编程导航网&#xff1a;https://www.code-nav.cn 大家好&#xff0c;我是鱼皮。 前两天我学编程的老弟小阿巴过生日&#xff0c;我问他想要什么礼物。 本来以为他会要什么游戏机、Q 币卡、鼠标键盘啥的&#xff0c;结果小阿巴…

网关、DHCP协议、ip地址、子网掩码简单介绍

参考文章&#xff1a;https://baike.baidu.com/item/%E7%BD%91%E5%85%B3/98992?frge_ala https://baike.baidu.com/item/DHCP%E6%9C%8D%E5%8A%A1%E5%99%A8/9956953?fromModulelemma_inlink https://blog.csdn.net/weixin_58783105/article/details/135041342 https://blog.cs…

Spring系列三:基于注解配置bean 下

基于注解配置bean &#x1f497;自动装配&#x1f35d;案例1: Autowired引出&#x1f35d;案例2: Autowired解读&#x1f35a;案例3: Resource解读&#x1f35d;小结 &#x1f497;泛型依赖注入&#x1f35d;基本说明&#x1f35d;应用实例 &#x1f497;自动装配 ●基本说明 …

数据中心巡检机器人助力,河南某数据中心机房智能化辅助项目交付

随着数据中心规模的不断扩大和业务需求的不断增长&#xff0c;确保其高效、安全、稳定地运行变得愈发重要。传统的人力巡检方式存在效率低、误差高、成本大等问题&#xff0c;难以满足现代数据中心的需求。为解决这些挑战&#xff0c;智能巡检机器人应运而生&#xff0c;成为数…

应用最优化方法及MATLAB实现——第3章代码实现

一、概述 在阅读最优方法及MATLAB实现后&#xff0c;想着将书中提供的代码自己手敲一遍&#xff0c;来提高自己对书中内容理解程度&#xff0c;巩固一下。 这部分内容主要针对第3章的内容&#xff0c;将其所有代码实现均手敲一遍&#xff0c;中间部分代码自己根据其公式有些许的…

Windows系统MySQL的安装,客户端工具Navicat的安装

下载mysql安装包&#xff0c;可以去官网下载&#xff1a;www.mysql.com。点击downloads 什么&#xff1f;后面还有福利&#xff1f; 下载MySQL 下载企业版&#xff1a; 下载Windows版 5点多的版本有点低&#xff0c;下载8.0.38版本的。Window系统。下载下面的企业版。不下载…

网安防御保护-小实验

1、DMZ区内的服务器&#xff0c;办公区仅能在办公时间内(9:00-18:00)可以访问&#xff0c;生产区的设备全天可以访问 2、生产区不允许访问互联网&#xff0c;办公区和游客区允许访问互联网 3、办公区设备10.0.2.10不允许访问DMZ区的FTP服务器和HTTP服务器&#xff0c;仅能ping通…

Android Settings应用 PreferenceScreen 条目隐藏实现和简单分析

Android Settings应用 PreferenceScreen 条目隐藏实现和简单分析 文章目录 Android Settings应用 PreferenceScreen 条目隐藏实现和简单分析一、前言二、隐藏实现1、xml 文件中隐藏PreferenceScreen 的某个条目2、普通Preference条目的隐藏的Java代码实现3、SwitchPreference条…

bevfomer self-att to transformer to tensorrt

self-attentation https://blog.csdn.net/weixin_42110638/article/details/134016569 query input* Wq key input* Wk value input* Wv output 求和 query . key * value detr multiScaleDeformableAttn Deformable Attention Module&#xff0c;在图像特征上&#…

STM32读取LX-224总线舵机信息

一、舵机指令包格式 帧头&#xff1a; 连续收到两个 0x55 ,表示有数据包到达。ID: 每个舵机都有一个 ID 号。ID 号范围 0&#xff5e;253,转换为十六进制 0x00&#xff5e;0xFD。广播 ID: ID 号 254(0xFE) 为广播 ID,若控制器发出的 ID 号为 254(0xFE)&#xff0c;所有的舵机均…

DockerFile文件解析

DockerFile 要研究自己如何做一个镜像&#xff0c;而且微服务项目打包上云部署&#xff0c;Docker就是最方便的。 微服务打包成镜像&#xff0c;任何装了Docker的地方&#xff0c;都可以下载使用&#xff0c;极其的方便。 流程&#xff1a;开发应用>DockerFile>打包为…

c++ 建造者模式

文章目录 建造者模式为什么使用建造者模式建造者模式实现步骤实现示例建造者模式优缺点 建造者模式 建造者模式&#xff08;Builder Pattern&#xff09;是面向对象设计模式中的一种&#xff0c;主要用于创建复杂对象。这种模式将对象的构建过程与其表示分离&#xff0c;允许用…

Qt+ESP32+SQLite 智能大棚

环境简介 硬件环境 ESP32、光照传感器、温湿度传感器、继电器、蜂鸣器 基本工作流程 上位机先运行&#xff0c;下位机启动后尝试连接上位机连接成功后定时上报传感器数据到上位机&#xff0c;上位机将信息进行处理展示判断下位机传感器数据&#xff0c;如果超过设置的阈值&a…

电商出海第一步,云手机或成重要因素

电商出海第一步并非易事&#xff0c;挑战和机遇并存&#xff0c;出海企业或个人或将借助云手机从而达成商业部署全球化的目的&#xff1b; 下面我们从网络稳定、数据安全、成本、以及多平台适配方面来看&#xff0c;究竟为什么说云手机会成为出海的重要因素&#xff1b; 首先…

图谱动态240709

本期将分享近期全球知识图谱相关 行业动态、会议资讯、论文推荐 —--| 行业动态 |--— 微软开源GraphRAG 7月2日&#xff0c;微软开源了GraphRAG&#xff0c;一种基于图的检索增强生成(RAG) 方法&#xff0c;可以对私有或以前未见过的数据集进行问答&#xff08;开源地址&…

ENSP防火墙

实验拓扑图 需求&#xff1a; ENSP的配置&#xff1a; 防火墙&#xff1a; 交换机&#xff1a; 华为防火墙的配置&#xff1a; 接口配置&#xff1a; 安全区域&#xff1a; 安全策略&#xff1a; 办公区访问DMZ&#xff1a; 生产区访问DMZ&#xff1a; 游客区只能访问门户网…

USB转RS485+RS232+TTL串口电路

USB转RS485RS232TTL电路 USB转RS485RS232TTL电路如下图所示&#xff0c;可实现USB转RS485RS232TTL串口&#xff0c;一个电路模块即可实现电路调试过程中用到常用接口。 电路模块上留有2.54MM单排针接口和接线端子两种接线方式&#xff0c;可接线和跳线。电路模块同时有5V和3.3V…

Vulkan入门系列0- Vulkan与OpenGL的区别

一:概述 Vulkan 是新一代图形和计算API,是由科纳斯组织(Khronos Group)维护的一套跨平台的、开放标准的、现代GPU 的编程接口,它仅仅是规定了一套编程接口,并没有接口的具体实现,实现是由硬件厂商适配实现的,市面上像NVIDIA、AMD和Intel等国际大厂基本提供了完整的…

C++ enum class转常量

当使用 enum class 时&#xff0c;它具有更强的类型安全性和隔离性&#xff0c;因此需要显式转换才能访问其底层整数值。 std::underlying_type_t 是一个类型别名&#xff0c;它返回枚举类型的底层类型。 to_underlying 函数提供了一种方便的方式来执行这种转换&#xff0c;特别…