commit:同步2.0版本

This commit is contained in:
Jerry
2021-10-20 19:52:20 +08:00
parent 668f3f99a9
commit 22a8d99f37
237 changed files with 10044 additions and 4360 deletions

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>framework</artifactId>
<groupId>com.orange.demo</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>apidoc-tools</artifactId>
<version>1.0.0</version>
<name>apidoc-tools</name>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.orange.demo</groupId>
<artifactId>common-core</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.thoughtworks.qdox</groupId>
<artifactId>qdox</artifactId>
<version>${qdox.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,28 @@
package com.orange.demo.apidoc.tools;
import com.alibaba.fastjson.JSON;
import com.orange.demo.apidoc.tools.codeparser.ApiCodeConfig;
import com.orange.demo.apidoc.tools.codeparser.ApiCodeParser;
import com.orange.demo.apidoc.tools.export.ApiPostmanExporter;
import freemarker.template.TemplateException;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
public class ExportApiApp {
public static void main(String[] args) throws IOException, TemplateException {
// 在第一次导出时需要打开export-api-config.json配置文件
// 修改其中的工程根目录配置项(projectRootPath),其他配置保持不变即可。
InputStream in = ExportApiApp.class.getResourceAsStream("/export-api-config.json");
String jsonData = StreamUtils.copyToString(in, StandardCharsets.UTF_8);
ApiCodeConfig apiCodeConfig = JSON.parseObject(jsonData, ApiCodeConfig.class);
ApiCodeParser apiCodeParser = new ApiCodeParser(apiCodeConfig);
ApiCodeParser.ApiProject project = apiCodeParser.doParse();
ApiPostmanExporter exporter = new ApiPostmanExporter();
// 将下面的目录改为实际输出目录。
exporter.doGenerate(project, "/xxx/Desktop/1.json");
}
}

View File

@@ -0,0 +1,28 @@
package com.orange.demo.apidoc.tools;
import com.alibaba.fastjson.JSON;
import com.orange.demo.apidoc.tools.codeparser.ApiCodeConfig;
import com.orange.demo.apidoc.tools.codeparser.ApiCodeParser;
import com.orange.demo.apidoc.tools.export.ApiDocExporter;
import freemarker.template.TemplateException;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
public class ExportDocApp {
public static void main(String[] args) throws IOException, TemplateException {
// 在第一次导出时需要打开export-api-config.json配置文件
// 修改其中的工程根目录配置项(projectRootPath),其他配置保持不变即可。
InputStream in = ExportDocApp.class.getResourceAsStream("/export-api-config.json");
String jsonData = StreamUtils.copyToString(in, StandardCharsets.UTF_8);
ApiCodeConfig apiCodeConfig = JSON.parseObject(jsonData, ApiCodeConfig.class);
ApiCodeParser apiCodeParser = new ApiCodeParser(apiCodeConfig);
ApiCodeParser.ApiProject project = apiCodeParser.doParse();
ApiDocExporter exporter = new ApiDocExporter();
// 将下面的目录改为实际输出目录。
exporter.doGenerate(project, "/xxx/Desktop/2.md");
}
}

View File

@@ -0,0 +1,83 @@
package com.orange.demo.apidoc.tools.codeparser;
import lombok.Data;
import java.util.List;
import java.util.Set;
/**
* 解析项目中接口信息的配置对象。
*
* @author Jerry
* @date 2020-09-24
*/
@Data
public class ApiCodeConfig {
/**
* 项目名称。
*/
private String projectName;
/**
* 项目的基础包名,如(com.demo.multi)。
*/
private String basePackage;
/**
* 项目在本地文件系统中的根目录。这里需要注意的是Windows用户请务必使用反斜杠作为目录分隔符。
* 如:"e:/mypath/OrangeSingleDemo""/Users/xxx/OrangeSingleDemo"。
*/
private String projectRootPath;
/**
* 是否为微服务项目。
*/
private Boolean microService;
/**
* 服务配置列表。对于单体服务至少也会有一个ServiceConfig对象。
*/
private List<ServiceConfig> serviceList;
@Data
public static class ServiceConfig {
/**
* 服务名称。
*/
private String serviceName;
/**
* 服务中文显示名称。
*/
private String showName;
/**
* 服务所在目录,相对于工程目录的子目录。
*/
private String servicePath;
/**
* 仅用于微服务工程。通常为服务路由路径,如:/admin/coursepaper。服务内的接口都会加上该路径前缀。
*/
private String serviceRequestPath;
/**
* 服务的端口号。
*/
private String port;
/**
* Api Controller信息列表。
*/
private List<ControllerInfo> controllerInfoList;
}
@Data
public static class ControllerInfo {
/**
* Controller.java等接口文件的所在目录。该目录仅为相对于服务代码目录的子目录。
* 目录分隔符请务必使用反斜杠。如:"/com/orange/demo/app/controller"。
*/
private String path;
/**
* 如果一个服务内存在多个Controller目录将再次生成二级子目录目录名为groupName。(可使用中文)
*/
private String groupName;
/**
* 在当前Controller目录下需要忽略的Controller列表 (只写类名即可)。如LoginController。
*/
private Set<String> skipControllers;
}
}

View File

@@ -0,0 +1,672 @@
package com.orange.demo.apidoc.tools.codeparser;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.orange.demo.common.core.object.Tuple2;
import com.orange.demo.apidoc.tools.exception.ApiCodeConfigParseException;
import com.thoughtworks.qdox.JavaProjectBuilder;
import com.thoughtworks.qdox.model.*;
import com.thoughtworks.qdox.model.impl.DefaultJavaParameterizedType;
import lombok.Data;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* 解析项目中的接口信息以及关联的Model、Dto和Mapper主要用于生成接口文档。
*
* @author Jerry
* @date 2020-09-24
*/
public class ApiCodeParser {
private static final String PATH_SEPERATOR = "/";
private static final String REQUEST_MAPPING = "RequestMapping";
private static final String FULL_REQUEST_MAPPING = "org.springframework.web.bind.annotation.RequestMapping";
private static final String GET_MAPPING = "GetMapping";
private static final String FULL_GET_MAPPING = "org.springframework.web.bind.annotation.GetMapping";
private static final String POST_MAPPING = "PostMapping";
private static final String FULL_POST_MAPPING = "org.springframework.web.bind.annotation.PostMapping";
private static final String VALUE_PROP = "value";
private static final String REQUIRED_PROP = "required";
private static final String DELETED_COLUMN = "DeletedFlagColumn";
/**
* 忽略微服务间标准调用接口的导出。
*/
private static final Set<String> IGNORED_API_METHOD_SET = new HashSet<>(8);
static {
IGNORED_API_METHOD_SET.add("listByIds");
IGNORED_API_METHOD_SET.add("getById");
IGNORED_API_METHOD_SET.add("existIds");
IGNORED_API_METHOD_SET.add("existId");
IGNORED_API_METHOD_SET.add("deleteById");
IGNORED_API_METHOD_SET.add("deleteBy");
IGNORED_API_METHOD_SET.add("listBy");
IGNORED_API_METHOD_SET.add("listMapBy");
IGNORED_API_METHOD_SET.add("listByNotInList");
IGNORED_API_METHOD_SET.add("getBy");
IGNORED_API_METHOD_SET.add("countBy");
IGNORED_API_METHOD_SET.add("aggregateBy");
}
/**
* 基础配置。
*/
private ApiCodeConfig config;
/**
* 工程对象。
*/
private ApiProject apiProject;
/**
* 项目中所有的解析后Java文件key是Java对象的全名com.orange.demo.xxxx.Student。
*/
private final Map<String, JavaClass> projectJavaClassMap = new HashMap<>(128);
/**
* 存储服务数据。key为配置的serviceName。
*/
private final Map<String, InternalServiceData> serviceDataMap = new HashMap<>(8);
/**
* 构造函数。
*
* @param config 配置对象。
*/
public ApiCodeParser(ApiCodeConfig config) {
this.config = config;
// 验证配置中的数据是否正确,出现错误直接抛出运行时异常。
this.verifyConfigData();
// 将配置文件中所有目录相关的参数,全部规格化处理,后续的使用中不用再做处理了。
this.normalizeConfigPath();
for (ApiCodeConfig.ServiceConfig serviceConfig : config.getServiceList()) {
InternalServiceData serviceData = new InternalServiceData();
// 仅有微服务项目,需要添加服务路由路径。
if (StrUtil.isNotBlank(serviceConfig.getServiceRequestPath())) {
String serviceRequestPath = "";
if (!serviceRequestPath.equals(PATH_SEPERATOR)) {
serviceRequestPath = normalizePath(serviceConfig.getServiceRequestPath());
}
serviceData.setServiceRequestPath(serviceRequestPath);
}
serviceDataMap.put(serviceConfig.getServiceName(), serviceData);
}
}
/**
* 执行解析操作。
*
* @return 解析后的工程对象。
*/
public ApiProject doParse() throws IOException {
// 先把工程完整编译一遍以便工程内的Java对象的引用信息更加完整。
this.parseProject();
// 开始逐级推演。
apiProject = new ApiProject();
apiProject.setProjectName(config.getProjectName());
apiProject.setMicroService(config.getMicroService());
apiProject.setServiceList(new LinkedList<>());
for (ApiCodeConfig.ServiceConfig serviceConfig : config.getServiceList()) {
ApiService apiService = this.parseService(serviceConfig);
apiProject.getServiceList().add(apiService);
}
return apiProject;
}
private void parseProject() throws IOException {
JavaProjectBuilder javaProjectBuilder = new JavaProjectBuilder();
javaProjectBuilder.setEncoding(StandardCharsets.UTF_8.name());
javaProjectBuilder.addSourceTree(new File(config.getProjectRootPath()));
// 全部导入,便于后续解析中使用和检索。
for (JavaClass javaClass : javaProjectBuilder.getClasses()) {
projectJavaClassMap.put(javaClass.getFullyQualifiedName(), javaClass);
}
}
private ApiService parseService(ApiCodeConfig.ServiceConfig serviceConfig) {
InternalServiceData serviceData = serviceDataMap.get(serviceConfig.getServiceName());
ApiService apiService = new ApiService();
apiService.setServiceName(serviceConfig.getServiceName());
apiService.setShowName(serviceConfig.getShowName());
apiService.setPort(serviceConfig.getPort());
List<ApiCodeConfig.ControllerInfo> controllerInfoList = serviceConfig.getControllerInfoList();
// 准备解析接口文件
for (ApiCodeConfig.ControllerInfo controllerInfo : controllerInfoList) {
JavaProjectBuilder javaControllerBuilder = new JavaProjectBuilder();
javaControllerBuilder.addSourceTree(new File(controllerInfo.getPath()));
for (JavaClass javaClass : javaControllerBuilder.getClasses()) {
if (controllerInfo.getSkipControllers() != null
&& controllerInfo.getSkipControllers().contains(javaClass.getName())) {
continue;
}
ApiClass apiClass = this.parseApiClass(controllerInfo, javaClass.getFullyQualifiedName(), serviceData);
if (apiClass != null) {
// 如果配置中为当前ControllerInfo添加了groupName属性
// 所有的生成后接口都会位于serviceName/groupName子目录否则都直接位于当前服务的子目录。
if (StrUtil.isBlank(apiClass.getGroupName())) {
apiService.getDefaultGroupClassSet().add(apiClass);
} else {
Set<ApiClass> groupedClassList = apiService.getGroupedClassMap()
.computeIfAbsent(apiClass.getGroupName(), k -> new TreeSet<>());
groupedClassList.add(apiClass);
}
}
}
}
return apiService;
}
private ApiClass parseApiClass(
ApiCodeConfig.ControllerInfo controllerInfo,
String classFullname,
InternalServiceData serviceData) {
// 去包含工程全部Class的Map中找到当前ControllerClass。
// 之所以这样做主要是因为全工程分析controller文件会包含更多更精确的对象关联信息。
JavaClass controllerClass = this.projectJavaClassMap.get(classFullname);
List<JavaAnnotation> classAnnotations = controllerClass.getAnnotations();
boolean hasControllerAnnotation = false;
String requestPath = "";
for (JavaAnnotation annotation : classAnnotations) {
String annotationName = annotation.getType().getValue();
if (this.isRequestMapping(annotationName) && annotation.getNamedParameter(VALUE_PROP) != null) {
requestPath = StrUtil.removeAll(
annotation.getNamedParameter(VALUE_PROP).toString(), "\"");
if (requestPath.equals(PATH_SEPERATOR) || StrUtil.isBlank(requestPath)) {
requestPath = "";
} else {
requestPath = normalizePath(requestPath);
}
}
if (isController(annotationName)) {
hasControllerAnnotation = true;
}
}
if (!hasControllerAnnotation) {
return null;
}
requestPath = serviceData.getServiceRequestPath() + requestPath;
ApiClass apiClass = new ApiClass();
apiClass.setName(controllerClass.getName());
apiClass.setFullName(controllerClass.getFullyQualifiedName());
apiClass.setComment(controllerClass.getComment());
apiClass.setGroupName(controllerInfo.getGroupName());
apiClass.setRequestPath(requestPath);
List<ApiMethod> methodList = this.parseApiMethodList(apiClass, controllerClass);
apiClass.setMethodList(methodList);
return apiClass;
}
private boolean needToIgnore(JavaMethod method) {
return !method.isPublic() || method.isStatic() || IGNORED_API_METHOD_SET.contains(method.getName());
}
private List<ApiMethod> parseApiMethodList(ApiClass apiClass, JavaClass javaClass) {
List<ApiMethod> apiMethodList = new LinkedList<>();
List<JavaMethod> methodList = javaClass.getMethods();
for (JavaMethod method : methodList) {
if (this.needToIgnore(method)) {
continue;
}
List<JavaAnnotation> methodAnnotations = method.getAnnotations();
Tuple2<String, String> result = this.parseRequestPathAndHttpMethod(methodAnnotations);
String methodRequestPath = result.getFirst();
String httpMethod = result.getSecond();
if (StrUtil.isNotBlank(methodRequestPath)) {
ApiMethod apiMethod = new ApiMethod();
apiMethod.setName(method.getName());
apiMethod.setComment(method.getComment());
apiMethod.setHttpMethod(httpMethod);
methodRequestPath = StrUtil.removeAll(methodRequestPath, "\"");
methodRequestPath = apiClass.getRequestPath() + normalizePath(methodRequestPath);
apiMethod.setRequestPath(methodRequestPath);
apiMethod.setPathList(StrUtil.splitTrim(apiMethod.getRequestPath(), PATH_SEPERATOR));
if (apiMethod.getRequestPath().contains("/listDict")) {
apiMethod.setListDictUrl(true);
} else if (apiMethod.getRequestPath().endsWith("/list")
|| apiMethod.getRequestPath().endsWith("/listWithGroup")
|| apiMethod.getRequestPath().contains("/listNotIn")
|| apiMethod.getRequestPath().contains("/list")) {
apiMethod.setListUrl(true);
} else if (apiMethod.getRequestPath().contains("/doLogin")) {
apiMethod.setLoginUrl(true);
}
JavaClass returnClass = method.getReturns();
if (returnClass.isVoid()) {
apiMethod.setReturnString("void");
} else {
apiMethod.setReturnString(returnClass.getGenericValue());
}
apiMethodList.add(apiMethod);
List<ApiArgument> apiArgumentList = this.parseApiMethodArgumentList(method);
apiMethod.setArgumentList(apiArgumentList);
this.classifyArgumentList(apiMethod, apiArgumentList);
}
}
return apiMethodList;
}
private void classifyArgumentList(ApiMethod apiMethod, List<ApiArgument> apiArgumentList) {
for (ApiArgument arg : apiArgumentList) {
if (arg.getAnnotationType() == ApiArgumentAnnotationType.REQUEST_PARAM) {
if (arg.uploadFileParam) {
apiMethod.getUploadParamArgumentList().add(arg);
} else {
apiMethod.getQueryParamArgumentList().add(arg);
}
}
if (arg.getAnnotationType() != ApiArgumentAnnotationType.REQUEST_PARAM) {
apiMethod.getJsonParamArgumentList().add(arg);
}
}
}
private Tuple2<String, String> parseRequestPathAndHttpMethod(List<JavaAnnotation> methodAnnotations) {
for (JavaAnnotation annotation : methodAnnotations) {
String annotationName = annotation.getType().getValue();
if (GET_MAPPING.equals(annotationName) || FULL_GET_MAPPING.equals(annotationName)) {
String methodRequestPath = annotation.getNamedParameter(VALUE_PROP).toString();
String httpMethod = "GET";
return new Tuple2<>(methodRequestPath, httpMethod);
}
if (POST_MAPPING.equals(annotationName) || FULL_POST_MAPPING.equals(annotationName)) {
String methodRequestPath = annotation.getNamedParameter(VALUE_PROP).toString();
String httpMethod = "POST";
return new Tuple2<>(methodRequestPath, httpMethod);
}
}
return new Tuple2<>(null, null);
}
private List<ApiArgument> parseApiMethodArgumentList(JavaMethod javaMethod) {
List<ApiArgument> apiArgumentList = new LinkedList<>();
List<JavaParameter> parameterList = javaMethod.getParameters();
if (CollUtil.isEmpty(parameterList)) {
return apiArgumentList;
}
for (JavaParameter parameter : parameterList) {
String typeName = parameter.getType().getValue();
// 该类型的参数为Validator的验证结果对象因此忽略。
if ("BindingResult".equals(typeName) || this.isServletArgument(typeName)) {
continue;
}
ApiArgument apiArgument = this.parseApiMethodArgument(parameter);
apiArgumentList.add(apiArgument);
}
return apiArgumentList;
}
private String parseMethodArgmentComment(JavaParameter parameter) {
String comment = null;
JavaExecutable executable = parameter.getExecutable();
List<DocletTag> tags = executable.getTagsByName("param");
if (CollUtil.isNotEmpty(tags)) {
for (DocletTag tag : tags) {
if (tag.getValue().startsWith(parameter.getName())) {
comment = StrUtil.removePrefix(tag.getValue(), parameter.getName()).trim();
break;
}
}
}
return comment;
}
private ApiArgument parseApiMethodArgument(JavaParameter parameter) {
String typeName = parameter.getType().getValue();
ApiArgument apiArgument = new ApiArgument();
ApiArgumentAnnotation argumentAnnotation =
this.parseArgumentAnnotationTypeAndName(parameter.getAnnotations(), parameter.getName());
apiArgument.setAnnotationType(argumentAnnotation.getType());
apiArgument.setName(argumentAnnotation.getName());
apiArgument.setTypeName(typeName);
apiArgument.setFullTypeName(parameter.getFullyQualifiedName());
if (argumentAnnotation.getType() == ApiArgumentAnnotationType.REQUEST_PARAM) {
apiArgument.setRequired(argumentAnnotation.isRequired());
}
String comment = parseMethodArgmentComment(parameter);
apiArgument.setComment(comment);
// 文件上传字段,是必填参数。
if ("MultipartFile".equals(typeName)) {
apiArgument.setUploadFileParam(true);
apiArgument.setRequired(true);
return apiArgument;
}
// 对于内置类型,则无需继续处理了。所有和内置类型参数相关的处理,应该在之前完成。
if (this.verifyAndSetBuiltinParam(apiArgument, typeName)) {
return apiArgument;
}
// 判断是否为集合类型的参数。
if (this.isCollectionType(typeName)) {
apiArgument.setCollectionParam(true);
if (parameter.getType() instanceof DefaultJavaParameterizedType) {
DefaultJavaParameterizedType javaType = (DefaultJavaParameterizedType) parameter.getType();
JavaType genericType = javaType.getActualTypeArguments().get(0);
ApiModel apiModel = this.buildApiModelForArgument(genericType.getFullyQualifiedName());
apiArgument.setModelData(apiModel);
apiArgument.setFullTypeName(parameter.getGenericFullyQualifiedName());
apiArgument.setTypeName(parameter.getGenericValue());
}
} else {
ApiModel apiModel = this.buildApiModelForArgument(parameter.getFullyQualifiedName());
apiArgument.setModelData(apiModel);
}
return apiArgument;
}
private boolean verifyAndSetBuiltinParam(ApiArgument apiArgument, String typeName) {
if ("MyOrderParam".equals(typeName)) {
apiArgument.setOrderParam(true);
} else if ("MyPageParam".equals(typeName)) {
apiArgument.setPageParam(true);
} else if ("MyGroupParam".equals(typeName)) {
apiArgument.setGroupParam(true);
} else if ("MyQueryParam".equals(typeName)) {
apiArgument.setQueryParam(true);
} else if ("MyAggregationParam".equals(typeName)) {
apiArgument.setAggregationParam(true);
}
return apiArgument.isOrderParam()
|| apiArgument.isPageParam()
|| apiArgument.isGroupParam()
|| apiArgument.isQueryParam()
|| apiArgument.isAggregationParam();
}
private ApiArgumentAnnotation parseArgumentAnnotationTypeAndName(
List<JavaAnnotation> annotationList, String defaultName) {
ApiArgumentAnnotation argumentAnnotation = new ApiArgumentAnnotation();
argumentAnnotation.setType(ApiArgumentAnnotationType.REQUEST_PARAM);
argumentAnnotation.setName(defaultName);
for (JavaAnnotation annotation : annotationList) {
String annotationName = annotation.getType().getValue();
if ("RequestBody".equals(annotationName)) {
argumentAnnotation.setType(ApiArgumentAnnotationType.REQUEST_BODY);
return argumentAnnotation;
} else if ("MyRequestBody".equals(annotationName)) {
String annotationValue = this.getArgumentNameFromAnnotationValue(annotation, VALUE_PROP);
argumentAnnotation.setType(ApiArgumentAnnotationType.MY_REQUEST_BODY);
argumentAnnotation.setName(annotationValue != null ? annotationValue : defaultName);
return argumentAnnotation;
} else if ("RequestParam".equals(annotationName)) {
String annotationValue = this.getArgumentNameFromAnnotationValue(annotation, VALUE_PROP);
argumentAnnotation.setType(ApiArgumentAnnotationType.REQUEST_PARAM);
argumentAnnotation.setName(annotationValue != null ? annotationValue : defaultName);
String requiredValue = this.getArgumentNameFromAnnotationValue(annotation, REQUIRED_PROP);
if (StrUtil.isNotBlank(requiredValue)) {
argumentAnnotation.setRequired(Boolean.parseBoolean(requiredValue));
}
return argumentAnnotation;
}
}
// 缺省为@RequestParam
return argumentAnnotation;
}
private String getArgumentNameFromAnnotationValue(JavaAnnotation annotation, String attribute) {
Object value = annotation.getNamedParameter(attribute);
if (value == null) {
return null;
}
String paramAlias = value.toString();
if (StrUtil.isNotBlank(paramAlias)) {
paramAlias = StrUtil.removeAll(paramAlias, "\"");
}
return paramAlias;
}
private ApiModel buildApiModelForArgument(String fullJavaClassName) {
// 先从当前服务内的Model中找如果参数是Model类型的对象微服务和单体行为一致。
ApiModel apiModel = apiProject.getFullNameModelMap().get(fullJavaClassName);
if (apiModel != null) {
return apiModel;
}
// 判断工程全局对象映射中是否包括该对象类型,如果不包含,就直接返回了。
JavaClass modelClass = projectJavaClassMap.get(fullJavaClassName);
if (modelClass == null) {
return apiModel;
}
// 先行解析对象中的字段。
apiModel = parseModel(modelClass);
apiProject.getFullNameModelMap().put(fullJavaClassName, apiModel);
return apiModel;
}
private ApiModel parseModel(JavaClass javaClass) {
ApiModel apiModel = new ApiModel();
apiModel.setName(javaClass.getName());
apiModel.setFullName(javaClass.getFullyQualifiedName());
apiModel.setComment(javaClass.getComment());
apiModel.setFieldList(new LinkedList<>());
List<JavaField> fieldList = javaClass.getFields();
for (JavaField field : fieldList) {
if (field.isStatic()) {
continue;
}
ApiField apiField = new ApiField();
apiField.setName(field.getName());
apiField.setComment(field.getComment());
apiField.setTypeName(field.getType().getSimpleName());
apiModel.getFieldList().add(apiField);
}
return apiModel;
}
private void verifyConfigData() {
if (StrUtil.isBlank(config.getProjectName())) {
throw new ApiCodeConfigParseException("ProjectName field can't be EMPTY.");
}
if (StrUtil.isBlank(config.getBasePackage())) {
throw new ApiCodeConfigParseException("BasePackage field can't be EMPTY.");
}
if (StrUtil.isBlank(config.getProjectRootPath())) {
throw new ApiCodeConfigParseException("ProjectRootPath field can't be EMPTY.");
}
if (!FileUtil.exist(config.getProjectRootPath())) {
throw new ApiCodeConfigParseException(
"ProjectRootPath doesn't exist, please check ./resources/export-api-config.json as DEFAULT.");
}
if (config.getMicroService() == null) {
throw new ApiCodeConfigParseException("MicroService field can't be NULL.");
}
if (CollUtil.isEmpty(config.getServiceList())) {
throw new ApiCodeConfigParseException("ServiceList field can't be EMPTY.");
}
this.verifyServiceConfig(config.getServiceList());
}
private void verifyServiceConfig(List<ApiCodeConfig.ServiceConfig> serviceConfigList) {
Set<String> serviceNameSet = new HashSet<>(8);
Set<String> servicePathSet = new HashSet<>(8);
for (ApiCodeConfig.ServiceConfig serviceConfig : serviceConfigList) {
if (StrUtil.isBlank(serviceConfig.getServiceName())) {
throw new ApiCodeConfigParseException("One of the ServiceName Field in Services List is NULL.");
}
String serviceName = serviceConfig.getServiceName();
if (StrUtil.isBlank(serviceConfig.getServicePath())) {
throw new ApiCodeConfigParseException(
"The ServicePath Field in Service [" + serviceName + "] is NULL.");
}
if (serviceNameSet.contains(serviceName)) {
throw new ApiCodeConfigParseException("The ServiceName [" + serviceName + "] is duplicated.");
}
serviceNameSet.add(serviceName);
if (servicePathSet.contains(serviceConfig.getServicePath())) {
throw new ApiCodeConfigParseException(
"The ServicePath [" + serviceConfig.getServicePath() + "] is duplicated.");
}
servicePathSet.add(serviceConfig.getServicePath());
if (StrUtil.isBlank(serviceConfig.getPort())) {
throw new ApiCodeConfigParseException(
"The Port Field in Service [" + serviceName + "] is NULL.");
}
this.verifyServiceControllerConfig(serviceConfig.getControllerInfoList(), serviceName);
}
}
private void verifyServiceControllerConfig(
List<ApiCodeConfig.ControllerInfo> controllerInfoList, String serviceName) {
if (CollUtil.isEmpty(controllerInfoList)) {
throw new ApiCodeConfigParseException(
"The ControllerInfoList Field of Service [" + serviceName + "] is EMPTY");
}
for (ApiCodeConfig.ControllerInfo controllerInfo : controllerInfoList) {
if (StrUtil.isBlank(controllerInfo.getPath())) {
throw new ApiCodeConfigParseException(
"One of the ControllerInfo.Path Field of Service [" + serviceName + "] is EMPTY");
}
}
}
private void normalizeConfigPath() {
config.setProjectRootPath(normalizePath(config.getProjectRootPath()));
for (ApiCodeConfig.ServiceConfig serviceConfig : config.getServiceList()) {
serviceConfig.setServicePath(config.getProjectRootPath() + normalizePath(serviceConfig.getServicePath()));
for (ApiCodeConfig.ControllerInfo controllerInfo : serviceConfig.getControllerInfoList()) {
controllerInfo.setPath(serviceConfig.getServicePath() + normalizePath(controllerInfo.getPath()));
}
}
}
private String normalizePath(String path) {
if (!path.startsWith(PATH_SEPERATOR)) {
path = PATH_SEPERATOR + path;
}
return StrUtil.removeSuffix(path, PATH_SEPERATOR);
}
private boolean isCollectionType(String typeName) {
return "List".equals(typeName) || "Set".equals(typeName) || "Collection".equals(typeName);
}
private boolean isServletArgument(String typeName) {
return "HttpServletResponse".equals(typeName) || "HttpServletRequest".equals(typeName);
}
private boolean isController(String annotationName) {
return "Controller".equals(annotationName)
|| "org.springframework.stereotype.Controller".equals(annotationName)
|| "RestController".equals(annotationName)
|| "org.springframework.web.bind.annotation.RestController".equals(annotationName);
}
private boolean isRequiredColumn(String annotationName) {
return "NotNull".equals(annotationName)
|| "javax.validation.constraints.NotNull".equals(annotationName)
|| "NotBlank".equals(annotationName)
|| "javax.validation.constraints.NotBlank".equals(annotationName)
|| "NotEmpty".equals(annotationName)
|| "javax.validation.constraints.NotEmpty".equals(annotationName);
}
private boolean isRequestMapping(String name) {
return REQUEST_MAPPING.equals(name) || FULL_REQUEST_MAPPING.equals(name);
}
@Data
public static class ApiProject {
private String projectName;
private Boolean microService;
private List<ApiService> serviceList;
private Map<String, ApiModel> fullNameModelMap = new HashMap<>(32);
private Map<String, ApiModel> simpleNameModelMap = new HashMap<>(32);
}
@Data
public static class ApiService {
private String serviceName;
private String showName;
private String port;
private Set<ApiClass> defaultGroupClassSet = new TreeSet<>();
private Map<String, Set<ApiClass>> groupedClassMap = new LinkedHashMap<>();
}
@Data
public static class ApiClass implements Comparable<ApiClass> {
private String name;
private String fullName;
private String groupName;
private String comment;
private String requestPath;
private List<ApiMethod> methodList;
@Override
public int compareTo(ApiClass o) {
return this.name.compareTo(o.name);
}
}
@Data
public static class ApiMethod {
private String name;
private String comment;
private String returnString;
private String requestPath;
private String httpMethod;
private boolean listDictUrl = false;
private boolean listUrl = false;
private boolean loginUrl = false;
private List<String> pathList = new LinkedList<>();
private List<ApiArgument> argumentList;
private List<ApiArgument> queryParamArgumentList = new LinkedList<>();
private List<ApiArgument> jsonParamArgumentList = new LinkedList<>();
private List<ApiArgument> uploadParamArgumentList = new LinkedList<>();
}
@Data
public static class ApiArgument {
private String name;
private String typeName;
private String fullTypeName;
private String comment;
private Integer annotationType;
private boolean required = true;
private boolean uploadFileParam = false;
private boolean collectionParam = false;
private boolean orderParam = false;
private boolean pageParam = false;
private boolean groupParam = false;
private boolean queryParam = false;
private boolean aggregationParam = false;
private boolean jsonData = false;
private ApiModel modelData;
}
@Data
public static class ApiArgumentAnnotation {
private String name;
private Integer type;
private boolean required = true;
}
@Data
public static class ApiModel {
private String name;
private String fullName;
private String comment;
private List<ApiField> fieldList;
}
@Data
public static class ApiField {
private String name;
private String comment;
private String typeName;
private boolean requiredColumn = false;
}
public static final class ApiArgumentAnnotationType {
public static final int REQUEST_PARAM = 0;
public static final int REQUEST_BODY = 1;
public static final int MY_REQUEST_BODY = 2;
private ApiArgumentAnnotationType() {
}
}
@Data
private static class InternalServiceData {
private String serviceRequestPath = "";
}
}

View File

@@ -0,0 +1,27 @@
package com.orange.demo.apidoc.tools.exception;
/**
* 解析接口信息配置对象中的异常。
*
* @author Jerry
* @date 2020-09-24
*/
public class ApiCodeConfigParseException extends RuntimeException {
/**
* 构造函数。
*/
public ApiCodeConfigParseException() {
}
/**
* 构造函数。
*
* @param msg 错误信息。
*/
public ApiCodeConfigParseException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,27 @@
package com.orange.demo.apidoc.tools.exception;
/**
* 解析Mybatis XML Mapper中的异常。
*
* @author Jerry
* @date 2020-09-24
*/
public class MapperParseException extends RuntimeException {
/**
* 构造函数。
*/
public MapperParseException() {
}
/**
* 构造函数。
*
* @param msg 错误信息。
*/
public MapperParseException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,84 @@
package com.orange.demo.apidoc.tools.export;
import com.orange.demo.apidoc.tools.codeparser.ApiCodeParser;
import com.orange.demo.apidoc.tools.util.FreeMarkerUtils;
import freemarker.template.Configuration;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import freemarker.template.TemplateModelException;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* 根据代码解析后的工程对象数据导出到Markdown格式的接口文档文件。
*
* @author Jerry
* @date 2020-09-24
*/
public class ApiDocExporter {
private final Configuration config;
public ApiDocExporter() throws TemplateModelException {
config = new Configuration(Configuration.VERSION_2_3_28);
config.setNumberFormat("0.####");
config.setClassicCompatible(true);
config.setAPIBuiltinEnabled(true);
config.setClassForTemplateLoading(ApiPostmanExporter.class, "/templates/");
config.setDefaultEncoding("UTF-8");
config.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
config.setSharedVariable("freemarkerUtils", new FreeMarkerUtils());
config.unsetCacheStorage();
config.clearTemplateCache();
}
/**
* 生成Markdown格式的API接口文档。
*
* @param apiProject 解析后的工程对象。
* @param outputFile 生成后的、包含全路径的输出文件名。
* @throws IOException 文件操作异常。
* @throws TemplateException 模板实例化异常。
*/
public void doGenerate(ApiCodeParser.ApiProject apiProject, String outputFile) throws IOException, TemplateException {
Map<String, Object> paramMap = new HashMap<>(1);
paramMap.put("project", apiProject);
List<ApiCodeParser.ApiService> newServiceList = new LinkedList<>();
if (apiProject.getMicroService()) {
// 在微服务场景中我们需要把upms服务放到最前面显示。
for (ApiCodeParser.ApiService apiService : apiProject.getServiceList()) {
if ("upms".equals(apiService.getServiceName())) {
newServiceList.add(apiService);
break;
}
}
for (ApiCodeParser.ApiService apiService : apiProject.getServiceList()) {
if (!"upms".equals(apiService.getServiceName())) {
newServiceList.add(apiService);
}
}
} else {
ApiCodeParser.ApiService appService = apiProject.getServiceList().get(0);
ApiCodeParser.ApiService newUpmsService = new ApiCodeParser.ApiService();
newUpmsService.setDefaultGroupClassSet(appService.getGroupedClassMap().get("upms"));
newUpmsService.setServiceName("upms");
newUpmsService.setShowName("用户权限模块");
newServiceList.add(newUpmsService);
ApiCodeParser.ApiService newAppService = new ApiCodeParser.ApiService();
newAppService.setDefaultGroupClassSet(appService.getGroupedClassMap().get("app"));
newAppService.setServiceName("app");
newAppService.setShowName("业务应用模块");
newServiceList.add(newAppService);
}
apiProject.setServiceList(newServiceList);
FileUtils.forceMkdirParent(new File(outputFile));
config.getTemplate("./api-doc.md.ftl").process(paramMap, new FileWriter(outputFile));
}
}

View File

@@ -0,0 +1,53 @@
package com.orange.demo.apidoc.tools.export;
import com.orange.demo.apidoc.tools.codeparser.ApiCodeParser;
import com.orange.demo.apidoc.tools.util.FreeMarkerUtils;
import freemarker.template.Configuration;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import freemarker.template.TemplateModelException;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 根据代码解析后的工程对象数据导出到Postman支持的JSON格式的文件。
*
* @author Jerry
* @date 2020-09-24
*/
public class ApiPostmanExporter {
private final Configuration config;
public ApiPostmanExporter() throws TemplateModelException {
config = new Configuration(Configuration.VERSION_2_3_28);
config.setNumberFormat("0.####");
config.setClassicCompatible(true);
config.setAPIBuiltinEnabled(true);
config.setClassForTemplateLoading(ApiPostmanExporter.class, "/templates/");
config.setDefaultEncoding("UTF-8");
config.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
config.setSharedVariable("freemarkerUtils", new FreeMarkerUtils());
config.unsetCacheStorage();
config.clearTemplateCache();
}
/**
* 生成Postman支持的JSON文档。
* @param apiProject 解析后的工程对象。
* @param outputFile 生成后的、包含全路径的输出文件名。
* @throws IOException 文件操作异常。
* @throws TemplateException 模板实例化异常。
*/
public void doGenerate(ApiCodeParser.ApiProject apiProject, String outputFile) throws IOException, TemplateException {
Map<String, Object> paramMap = new HashMap<>(1);
paramMap.put("project", apiProject);
FileUtils.forceMkdirParent(new File(outputFile));
config.getTemplate("./postman_collection.json.ftl").process(paramMap, new FileWriter(outputFile));
}
}

View File

@@ -0,0 +1,28 @@
package com.orange.demo.apidoc.tools.util;
import java.util.UUID;
/**
* 仅供Freemarker模板内部使用的Java工具函数。
*
* @author Jerry
* @date 2020-09-24
*/
public class FreeMarkerUtils {
/**
* 生成GUID。
*
* @return 生成后的GUID。
*/
public static String generateGuid() {
return UUID.randomUUID().toString();
}
/**
* 私有构造函数,明确标识该常量类的作用。
*/
public FreeMarkerUtils() {
// FreeMarker的工具对象Sonarqube建议给出空构造的注释。
}
}

View File

@@ -0,0 +1,24 @@
{
"projectName": "橙单单体开源版",
"basePackage": "com.orange.demo",
"projectRootPath": "这里请使用当前工程的根目录e:/xxx/OrangeDemo 或者 /Users/xxx/OrangeDemo",
"microService": "false",
"serviceList": [
{
"serviceName": "application",
"showName": "应用服务模块",
"servicePath": "/application-webadmin",
"port": "8082",
"controllerInfoList": [
{
"path": "/src/main/java/com/orange/demo/webadmin/app/controller",
"groupName": "app"
},
{
"path": "/src/main/java/com/orange/demo/webadmin/upms/controller",
"groupName": "upms"
}
]
}
]
}

View File

@@ -0,0 +1,144 @@
## 用户登录
### 登录接口
#### 登录
- **URI:** /admin/upms/login/doLogin
- **Type:** GET
- **Content-Type:** multipart/form-data
- **Request-Headers:**
Name|Type|Description
--|--|--
Authorization|String|身份验证的Token
- **Request-Parameters:**
Parameter|Type|Required|Description
--|--|--|--
loginName|string|true|用户名
password|string|true|加密后的用户密码
#### 退出
- **URI:** /admin/upms/login/logout
- **Type:** POST
- **Content-Type:** application/json; chartset=utf-8
- **Request-Headers:**
Name|Type|Description
--|--|--
Authorization|String|身份验证的Token
#### 修改密码
- **URI:** /admin/upms/login/changePassword
- **Type:** POST
- **Content-Type:** application/json; chartset=utf-8
- **Request-Headers:**
Name|Type|Description
--|--|--
Authorization|String|身份验证的Token
- **Request-Parameters:**
Parameter|Type|Required|Description
--|--|--|--
oldPass|string|true|加密后的原用户密码
newPass|string|true|加密后的新用户密码
<#list project.serviceList as service>
## ${service.showName}
<#list service.defaultGroupClassSet as apiClass>
### ${apiClass.name}
<#list apiClass.methodList as apiMethod>
#### ${apiMethod.name}
- **URI:** ${apiMethod.requestPath}
- **Type:** ${apiMethod.httpMethod}
- **Content-Type:** <#if apiMethod.httpMethod == "GET" || apiMethod.queryParamArgumentList?size gt 0 || apiMethod.uploadParamArgumentList?size gt 0>multipart/form-data<#else>application/json; chartset=utf-8</#if>
- **Request-Headers:**
Name|Type|Description
--|--|--
Authorization|String|身份验证的Token
<#if apiMethod.queryParamArgumentList?size gt 0 || apiMethod.uploadParamArgumentList?size gt 0>
- **Request-Parameters:**
Parameter|Type|Required|Description
--|--|--|--
<#list apiMethod.queryParamArgumentList as apiArgument>
<#if apiArgument.modelData??>
<#list apiArgument.modelData.tableFieldList as apiField>
${apiField.name}|${apiField.typeName}|<#if apiMethod.listDictUrl>false<#else><#if apiField.requiredColumn>true<#else>false</#if></#if>|${apiField.comment}
</#list>
<#else>
${apiArgument.name}|${apiArgument.typeName}|<#if apiMethod.listDictUrl>false<#else><#if apiArgument.required>true<#else>false</#if></#if>|${apiArgument.comment}
</#if><#-- apiArgument.modelData?? -->
</#list>
</#if>
<#list apiMethod.uploadParamArgumentList as apiArgument>
${apiArgument.name}|File|true|${apiArgument.comment}
</#list>
<#if apiMethod.jsonParamArgumentList?size gt 0>
- **Request-Body:**
``` json
{
<#list apiMethod.jsonParamArgumentList as apiArgument>
<#if apiArgument.modelData??>
<#if apiArgument.collectionParam>
"${apiArgument.name}" : [
{
<#if apiMethod.listUrl>
<#list apiArgument.modelData.filteredFieldList as apiField>
"${apiField.name}" : "${apiField.typeName} | false | <#if apiField.name == "searchString">模糊搜索字符串。<#else>${apiField.comment}</#if>"<#if apiField_has_next>,</#if>
</#list>
<#else><#-- apiMethod.listUrl -->
<#list apiArgument.modelData.tableFieldList as apiField>
<#if !apiMethod.addUrl || !apiField.primaryKey>
"${apiField.name}" : "${apiField.typeName} | <#if apiField.requiredColumn>true<#else>false</#if> | ${apiField.comment}"<#if apiField_has_next>,</#if>
</#if>
</#list>
</#if><#-- apiMethod.listUrl -->
}
]<#if apiArgument_has_next>,</#if>
<#else><#-- apiArgument.collectionParam -->
"${apiArgument.name}" : {
<#if apiMethod.listUrl>
<#list apiArgument.modelData.filteredFieldList as apiField>
"${apiField.name}" : "${apiField.typeName} | false | <#if apiField.name == "searchString">模糊搜索字符串。<#else>${apiField.comment}</#if>"<#if apiField_has_next>,</#if>
</#list>
<#else><#-- apiMethod.listUrl -->
<#list apiArgument.modelData.tableFieldList as apiField>
<#if !apiMethod.addUrl || !apiField.primaryKey>
"${apiField.name}" : "${apiField.typeName} | <#if apiField.requiredColumn>true<#else>false</#if> | ${apiField.comment}"<#if apiField_has_next>,</#if>
</#if>
</#list>
</#if><#-- apiMethod.listUrl -->
}<#if apiArgument_has_next>,</#if>
</#if><#-- apiArgument.collectionParam -->
<#elseif apiArgument.orderParam>
"${apiArgument.name}" : [
{
"fieldName" : "String | false | 排序字段名",
"asc" : "Boolean | false | 是否升序"
}
]<#if apiArgument_has_next>,</#if>
<#elseif apiArgument.groupParam>
"${apiArgument.name}" : [
{
"fieldName" : "String | false | 分组字段名",
"aliasName" : "String | false | 分组字段别名",
"dateAggregateBy" : "String | false | 是否按照日期聚合,可选项(day|month|year)"
}
]<#if apiArgument_has_next>,</#if>
<#elseif apiArgument.pageParam>
"${apiArgument.name}" : {
"pageNum": "Integer | false | 分页页号",
"pageSize": "Integer | false | 每页数据量"
}<#if apiArgument_has_next>,</#if>
<#elseif apiArgument.queryParam || apiArgument.aggregationParam>
${apiArgument.name}" : {
}<#if apiArgument_has_next>,</#if>
<#else><#-- apiArgument.modelData?? -->
<#if apiArgument.collectionParam>
"${apiArgument.name}" : [ "${apiArgument.typeName} | ${apiArgument.required}<#if apiArgument.comment??> | ${apiArgument.comment}</#if>" ]<#if apiArgument_has_next>,</#if>
<#else>
"${apiArgument.name}" : "${apiArgument.typeName} | ${apiArgument.required}<#if apiArgument.comment??> | ${apiArgument.comment}</#if>"<#if apiArgument_has_next>,</#if>
</#if>
</#if><#-- apiArgument.modelData?? -->
</#list>
}
```
</#if>
</#list><#-- apiClass.methodList as apiMethod -->
</#list><#-- upmsClassList as apiClass -->
</#list>

View File

@@ -0,0 +1,42 @@
<#import "postman_common.ftl" as Common>
{
"info": {
"_postman_id": "92b51dc5-3611-49ac-8d94-a0718dba5bf1",
"name": "${project.projectName}",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
<#list project.serviceList as service>
{
"name": "${service.serviceName}",
"item": [
<#if service.groupedClassMap?size gt 0>
<#list service.groupedClassMap?keys as groupName>
<#assign groupedClassList=service.groupedClassMap[groupName] />
{
"name": "${groupName}",
"item": [
<#list groupedClassList as apiClass>
{
<@Common.generateControllerRequest service apiClass 7/>
}<#if apiClass_has_next>,</#if>
</#list>
],
"protocolProfileBehavior": {},
"_postman_isSubFolder": true
}<#if groupName_has_next>,</#if>
</#list>
</#if>
<#list service.defaultGroupClassSet as apiClass>
{
<@Common.generateControllerRequest service apiClass 5/>
}<#if apiClass_has_next>,</#if>
</#list>
],
"protocolProfileBehavior": {},
"_postman_isSubFolder": true
}<#if service_has_next>,</#if>
</#list><#-- project.serviceList as service -->
],
"protocolProfileBehavior": {}
}

View File

@@ -0,0 +1,120 @@
<#macro doIndent level><#if level != 0><#list 0..(level-1) as i> </#list></#if></#macro>
<#macro generateControllerRequest service apiClass indentLevel>
<@doIndent indentLevel/>"name": "${apiClass.name}",
<@doIndent indentLevel/>"item": [
<#list apiClass.methodList as apiMethod>
<@doIndent indentLevel/> {
<@doIndent indentLevel/> "name": "${apiMethod.name}",
<#if apiMethod.loginUrl>
<@doIndent indentLevel/> "event": [
<@doIndent indentLevel/> {
<@doIndent indentLevel/> "listen": "test",
<@doIndent indentLevel/> "script": {
<@doIndent indentLevel/> "id": "${freemarkerUtils.generateGuid()}",
<@doIndent indentLevel/> "type": "text/javascript",
<@doIndent indentLevel/> "exec": [
<@doIndent indentLevel/> "pm.test(\"登录操作\", function () {",
<@doIndent indentLevel/> " var jsonData = pm.response.json();",
<@doIndent indentLevel/> " var token = jsonData.data.tokenData;",
<@doIndent indentLevel/> " pm.environment.set(\"token\", token);",
<@doIndent indentLevel/> " console.log(\"login token \" + token);",
<@doIndent indentLevel/> "});",
<@doIndent indentLevel/> ""
<@doIndent indentLevel/> ]
<@doIndent indentLevel/> }
<@doIndent indentLevel/> },
<@doIndent indentLevel/> {
<@doIndent indentLevel/> "listen": "prerequest",
<@doIndent indentLevel/> "script": {
<@doIndent indentLevel/> "id": "${freemarkerUtils.generateGuid()}",
<@doIndent indentLevel/> "type": "text/javascript",
<@doIndent indentLevel/> "exec": [
<@doIndent indentLevel/> ""
<@doIndent indentLevel/> ]
<@doIndent indentLevel/> }
<@doIndent indentLevel/> }
<@doIndent indentLevel/> ],
</#if>
<@doIndent indentLevel/> "request": {
<@doIndent indentLevel/> "method": "${apiMethod.httpMethod}",
<#if apiMethod.loginUrl>
<@doIndent indentLevel/> "header": [],
<#else>
<@doIndent indentLevel/> "header": [
<@doIndent indentLevel/> {
<@doIndent indentLevel/> "key": "Authorization",
<@doIndent indentLevel/> "value": "{{token}}",
<@doIndent indentLevel/> "type": "text"
<@doIndent indentLevel/> }
<@doIndent indentLevel/> ],
</#if>
<@doIndent indentLevel/> "url": {
<@doIndent indentLevel/> "raw": "http://{{host}}:${service.port}/${apiMethod.requestPath}",
<@doIndent indentLevel/> "protocol": "http",
<@doIndent indentLevel/> "host": [
<@doIndent indentLevel/> "{{host}}"
<@doIndent indentLevel/> ],
<@doIndent indentLevel/> "port": "${service.port}",
<@doIndent indentLevel/> "path": [
<#list apiMethod.pathList as path>
<@doIndent indentLevel/> "${path}"<#if path_has_next>,</#if>
</#list>
<@doIndent indentLevel/> ]<#if apiMethod.queryParamArgumentList?size gt 0>,</#if>
<#if apiMethod.queryParamArgumentList?size gt 0>
<@doIndent indentLevel/> "query": [
<#list apiMethod.queryParamArgumentList as apiArgument>
<#if apiArgument.modelData??>
<#list apiArgument.modelData.tableFieldList as apiField>
<@doIndent indentLevel/> {
<@doIndent indentLevel/> "key": "${apiField.name}",
<@doIndent indentLevel/> "value": ""
<@doIndent indentLevel/> }<#if apiArgument_has_next || apiField_has_next>,</#if>
</#list>
<#else>
<@doIndent indentLevel/> {
<@doIndent indentLevel/> "key": "${apiArgument.name}",
<@doIndent indentLevel/> "value": ""
<@doIndent indentLevel/> }<#if apiArgument_has_next>,</#if>
</#if>
</#list>
<@doIndent indentLevel/> ]
</#if>
<@doIndent indentLevel/> }<#if (apiMethod.httpMethod == "POST" && apiMethod.jsonParamArgumentList?size gt 0) || apiMethod.uploadParamArgumentList?size gt 0>,</#if>
<#if apiMethod.uploadParamArgumentList?size gt 0>
<@doIndent indentLevel/> "body": {
<@doIndent indentLevel/> "mode": "formdata",
<@doIndent indentLevel/> "formdata": [
<#list apiMethod.uploadParamArgumentList as apiArgument>
<@doIndent indentLevel/> {
<@doIndent indentLevel/> "key": "${apiArgument.name}",
<@doIndent indentLevel/> "type": "file",
<@doIndent indentLevel/> "src": []
<@doIndent indentLevel/> }<#if apiArgument_has_next>,</#if>
</#list>
<@doIndent indentLevel/> ]
<@doIndent indentLevel/> }<#if apiMethod.httpMethod == "POST" && apiMethod.jsonParamArgumentList?size gt 0>,</#if>
</#if><#-- apiMethod.uploadParamArgumentList?size gt 0 -->
<#if apiMethod.httpMethod == "POST" && apiMethod.jsonParamArgumentList?size gt 0>
<@doIndent indentLevel/> "body": {
<@doIndent indentLevel/> "mode": "raw",
<#if !apiMethod.loginUrl>
<@doIndent indentLevel/> "raw": "{\n<#list apiMethod.jsonParamArgumentList as apiArgument><#if apiArgument.modelData??><#if apiArgument.collectionParam>\t\"${apiArgument.name}\" : [\n\t\t{\n<#list apiArgument.modelData.fieldList as apiField><#if apiMethod.listUrl>\t\t\t\"${apiField.name}\" : \"\"<#if apiField_has_next>,</#if>\n<#else>\t\t\t\"${apiField.name}\" : \"<#if apiField.typeName == "Integer" || apiField.typeName == "Long">0</#if>\"<#if apiField_has_next>,</#if>\n</#if><#-- apiMethod.listUrl --></#list>\t\t}\n\t]<#if apiArgument_has_next>,</#if>\n<#else><#-- apiArgument.collectionParam -->\t\"${apiArgument.name}\" : {\n<#list apiArgument.modelData.fieldList as apiField><#if apiMethod.listUrl>\t\t\"${apiField.name}\" : \"\"<#if apiField_has_next>,</#if>\n<#else>\t\t\"${apiField.name}\" : \"<#if apiField.typeName == "Integer" || apiField.typeName == "Long">0</#if>\"<#if apiField_has_next>,</#if>\n</#if><#-- apiMethod.listUrl --></#list>\t}<#if apiArgument_has_next>,</#if>\n</#if><#-- apiArgument.collectionParam --><#elseif apiArgument.orderParam>\t\"${apiArgument.name}\" : [\n\t\t{\n\t\t\t\"fieldName\" : \"\",\n\t\t\t\"asc\" : \"true\"\n\t\t}\n\t]<#if apiArgument_has_next>,</#if>\n<#elseif apiArgument.groupParam>\t\"${apiArgument.name}\" : [\n\t\t{\n\t\t\t\"fieldName\" : \"\",\n\t\t\t\"aliasName\" : \"\",\n\t\t\t\"dateAggregateBy\" : \"\"\n\t\t}\n\t]<#if apiArgument_has_next>,</#if>\n<#elseif apiArgument.pageParam>\t\"${apiArgument.name}\" : {\n\t\t\"pageNum\": \"1\",\n\t\t\"pageSize\": \"10\"\n\t}<#if apiArgument_has_next>,</#if>\n<#elseif apiArgument.queryParam || apiArgument.aggregationParam>\t\"${apiArgument.name}\" : {\n\t}<#if apiArgument_has_next>,</#if>\n<#else><#if apiArgument.collectionParam>\t\"${apiArgument.name}\" : [ ]<#if apiArgument_has_next>,</#if>\n<#else>\t\"${apiArgument.name}\" : \"\"<#if apiArgument_has_next>,</#if>\n</#if></#if><#-- apiArgument.modelData?? --></#list><#-- apiMethod.jsonParamArgumentList?size gt 0 -->}\n",
<#else>
<@doIndent indentLevel/> "raw": "{\n \"loginName\":\"admin\",\n \"password\":\"IP3ccke3GhH45iGHB5qP9p7iZw6xUyj28Ju10rnBiPKOI35sc%2BjI7%2FdsjOkHWMfUwGYGfz8ik31HC2Ruk%2Fhkd9f6RPULTHj7VpFdNdde2P9M4mQQnFBAiPM7VT9iW3RyCtPlJexQ3nAiA09OqG%2F0sIf1kcyveSrulxembARDbDo%3D\"\n}",
</#if>
<@doIndent indentLevel/> "options": {
<@doIndent indentLevel/> "raw": {
<@doIndent indentLevel/> "language": "json"
<@doIndent indentLevel/> }
<@doIndent indentLevel/> }
<@doIndent indentLevel/> }
</#if>
<@doIndent indentLevel/> },
<@doIndent indentLevel/> "response": []
<@doIndent indentLevel/> }<#if apiMethod_has_next>,</#if>
</#list><#-- apiClass.methodList as apiMethod -->
<@doIndent indentLevel/>],
<@doIndent indentLevel/>"protocolProfileBehavior": {},
<@doIndent indentLevel/>"_postman_isSubFolder": true
</#macro>