Browse Source

v4

master
583641232@qq.com 8 months ago
parent
commit
33889b72f9
  1. 5
      client/pom.xml
  2. 26
      client/src/main/java/com/inscloudtech/alog/client/task/ALogConfig.java
  3. 9
      client/src/main/java/com/inscloudtech/alog/client/task/Monitor.java
  4. 20
      common/src/main/java/com/inscloudtech/alog/common/annotation/ActionLog.java
  5. 21
      common/src/main/java/com/inscloudtech/alog/common/annotation/IgnoreRecordField.java
  6. 2
      common/src/main/java/com/inscloudtech/alog/common/annotation/NeedRecordField.java
  7. 23
      common/src/main/java/com/inscloudtech/alog/common/utils/IdWorker.java
  8. 5
      example/pom.xml
  9. 140
      example/src/main/java/com/inscloudtech/alog/clientdemo/aspectj/ActionLogAspect.java
  10. 25
      example/src/main/java/com/inscloudtech/alog/clientdemo/demo/controller/DemoUserController.java
  11. 72
      example/src/main/java/com/inscloudtech/alog/clientdemo/utils/BeanCompareUtils.java
  12. 111
      example/src/main/java/com/inscloudtech/alog/clientdemo/utils/BeanCopyUtils.java
  13. 1
      example/src/main/java/com/inscloudtech/alog/clientdemo/utils/StringUtils.java
  14. 6
      example/src/main/resources/application.yml
  15. 10
      worker/pom.xml
  16. 4
      worker/src/main/java/com/inscloudtech/alog/worker/disruptor/TracerConsumer.java
  17. 12
      worker/src/main/resources/application.yml
  18. 2
      worker/src/main/resources/jlog.sql

5
client/pom.xml

@ -17,6 +17,11 @@
<artifactId>alog-common</artifactId>
<version>0.1.1</version>
</dependency>
<!-- SpringBoot 拦截器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- <dependency>
<groupId>com.inscloudtech</groupId>

26
client/src/main/java/com/inscloudtech/alog/client/task/ALogConfig.java

@ -0,0 +1,26 @@
package com.inscloudtech.alog.client.task;
import java.util.List;
public class ALogConfig {
private long appId;
private List<String> workers;
public long getAppId() {
return appId;
}
public void setAppId(long appId) {
this.appId = appId;
}
public List<String> getWorkers() {
return workers;
}
public void setWorkers(List<String> workers) {
this.workers = workers;
}
}

9
client/src/main/java/com/inscloudtech/alog/client/task/Monitor.java

@ -1,7 +1,9 @@
package com.inscloudtech.alog.client.task;
import com.inscloudtech.alog.client.tracerholder.TracerHolder;
import com.inscloudtech.alog.client.worker.WorkerInfoHolder;
import com.inscloudtech.alog.common.utils.IdWorker;
import com.inscloudtech.alog.core.Configurator;
import com.inscloudtech.alog.core.ConfiguratorFactory;
import org.slf4j.Logger;
@ -59,11 +61,12 @@ public class Monitor {
private void fetch() throws Exception {
Configurator configurator = ConfiguratorFactory.getInstance();
//获取所有worker的ip
List<String> addresses;
try {
//如果设置了机房属性则拉取同机房的worker如果同机房没worker则拉取所有
addresses = configurator.getList("aLog-workers");
// addresses = configurator.getString("aLog-workers");
ALogConfig aLogConfig = configurator.getObject("aLog", ALogConfig.class);
List<String> addresses = aLogConfig.getWorkers();
IdWorker.setAppIdId(aLogConfig.getAppId());
//全是空给个警告
if (addresses == null || addresses.isEmpty()) {
LOGGER.warn("very important warn !!! workers ip info is null!!!");

20
common/src/main/java/com/inscloudtech/alog/common/annotation/ActionLog.java

@ -28,14 +28,6 @@ public @interface ActionLog {
* 操作类别
*/
BusinessType businessType() default BusinessType.OTHER;
/**
* 是否保存请求的参数
*/
boolean isSaveRequestData() default true;
/**
* 是否保存响应的参数
*/
boolean isSaveResponseData() default true;
/**
* 排除指定的请求参数
*/
@ -46,12 +38,18 @@ public @interface ActionLog {
*/
Class<?> mapperClass() default Object.class;
/**
* 修改实体类
*/
Class<?> entityClass() default Object.class;
/**
* 用来获取数据修改前对象的方法
* @return
* 用来获取数据修改前对象的方法名称
*/
String methodName() default "selectById";
/**
* 实体类id字段名称
*/
String idFieldName() default "id";
}

21
common/src/main/java/com/inscloudtech/alog/common/annotation/IgnoreRecordField.java

@ -0,0 +1,21 @@
package com.inscloudtech.alog.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 忽略记录的修改值
*
* @author inscloudtech
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
//@JacksonAnnotationsInside
//@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface IgnoreRecordField {
String fieldName() default "";
}

2
common/src/main/java/com/inscloudtech/alog/common/annotation/NeedRecordField.java

@ -26,3 +26,5 @@ import java.lang.annotation.Target;
public @interface NeedRecordField {
String fieldName() default "";
}

23
common/src/main/java/com/inscloudtech/alog/common/utils/IdWorker.java

@ -30,6 +30,8 @@ public class IdWorker {
private static long workerId;
private static long appId;
static {
Calendar calendar = Calendar.getInstance();
calendar.set(2017, Calendar.APRIL, 1);
@ -85,6 +87,10 @@ public class IdWorker {
}
}
public static void setAppIdId(final Long appId) {
IdWorker.appId = appId;
}
//下一个ID生成算法
public static long nextId() {
long time = System.currentTimeMillis();
@ -109,4 +115,21 @@ public class IdWorker {
}
return time;
}
//下一个ID生成算法
public static long nextIdWithAppId() {
long time = System.currentTimeMillis();
if (lastTime > time) {
throw new RuntimeException("Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds" + lastTime);
}
if (lastTime == time) {
if (0L == (sequence = ++sequence & SEQUENCE_MASK)) {
time = waitUntilNextTime(time);
}
} else {
sequence = 0;
}
lastTime = time;
return appId | ((time - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (workerId << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
}
}

5
example/pom.xml

@ -30,11 +30,6 @@
</exclusions>
</dependency>
<!-- SpringBoot 拦截器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.inscloudtech</groupId>

140
example/src/main/java/com/inscloudtech/alog/clientdemo/aspectj/ActionLogAspect.java

@ -2,8 +2,6 @@ package com.inscloudtech.alog.clientdemo.aspectj;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.inscloudtech.alog.client.tracerholder.TracerHolder;
import com.inscloudtech.alog.clientdemo.utils.*;
import com.inscloudtech.alog.common.enums.BusinessStatus;
import com.inscloudtech.alog.common.enums.BusinessType;
@ -23,10 +21,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@ -34,7 +29,7 @@ import java.util.Map;
import java.util.stream.Collectors;
/**
* 操作日志记录处理
* 行为日志记录处理
*
* @author inscloudtech
*/
@ -46,27 +41,54 @@ public class ActionLogAspect {
public Object doAround(ProceedingJoinPoint joinPoint, ActionLog actionLog) throws Throwable {
ActionLogMessage actionLogMessage = new ActionLogMessage();
Object[] args = joinPoint.getArgs();
Object arg = args[0];
//当操作行为是修改数据时需要上报修改前后数据值
if(actionLog.businessType().equals(BusinessType.UPDATE)){
Class<?> mapperClass = actionLog.mapperClass();
//service,mapper对应的实体类
Class<?> entityClass = null;
boolean isMapperClass = BaseMapper.class.isAssignableFrom(mapperClass);
//不是service类
if(isMapperClass){
//获取Mapper对应的entity
Type superClass = mapperClass.getGenericInterfaces()[0];
if (superClass instanceof ParameterizedType) {
// 获取类型参数数组 [<当前接口的 Class 对象>, <SysUser Class 对象>, <SysUserVo Class 对象>]
Type[] typeArr = ((ParameterizedType) superClass).getActualTypeArguments();
// 第二个类型参数就是 SysUser Class 对象
entityClass = (Class<?>) typeArr[1];
setBeforeAfterValue(args,actionLog,actionLogMessage);
}
}else{
entityClass = actionLog.entityClass();
Object result;
long startTime = System.currentTimeMillis();
try {
//执行方法
result = joinPoint.proceed(args);
} catch (Exception e) {
throw e;
}
// 记录结束时间
long endTime = System.currentTimeMillis();
// 计算响应时间
long responseTime = endTime - startTime;
actionLogMessage.setResponseTime(responseTime);
// 请求的地址
String ip = ServletUtils.getClientIP();
actionLogMessage.setOperIp(ip);
actionLogMessage.setOperUrl(StringUtils.sub(ServletUtils.getRequest().getRequestURI(), 0, 255));
//从业务系统获取传入
// LoginUser loginUser = LoginHelper.getLoginUser();//获取当前用户应用自行实现
// loginUser.getUserId();
// loginUser.getNickname();
// loginUser.getDeptName();
actionLogMessage.setOperUserId("2322434253");
actionLogMessage.setOperUserName("演示用户");
actionLogMessage.setDeptName("演示部门");
handleLog(joinPoint, actionLog, null, result,actionLogMessage);
return result;
}
/**
* 示例是基于mybatis-plus mapper类的的selectById方法获取数据库值与修改后比较变化值并记录
* 应用方可重写该方法最终能获取到
* 更新前值beforeValue更新新后值afterValue业务数据idbusinessId
* 即可
*/
private void setBeforeAfterValue(Object[] args,ActionLog actionLog,ActionLogMessage actionLogMessage){
Object arg = args[0];
Class<?> mapperClass = actionLog.mapperClass();
Class<?> entityClass = actionLog.entityClass();
//获取tableId字段
String tableIdField = "";
@ -79,6 +101,7 @@ public class ActionLogAspect {
}
}
Object afterEntity = null;
Long businessId = null;
try {
// arg 转换为 entityClass 类型的对象
afterEntity = entityClass.cast(arg);
@ -86,23 +109,27 @@ public class ActionLogAspect {
Field idField = entityClass.getDeclaredField(tableIdField);
idField.setAccessible(true);
// 获取 id 的值
Object idValue = idField.get(afterEntity);
System.out.println("ID 的值为: " + idValue);
businessId = (Long)idField.get(afterEntity);
} catch (IllegalAccessException | NoSuchFieldException e) {
e.printStackTrace();
}
Long businessId = 0L;
//根据mapper查询一条修改前的数据
Object serviceObj = SpringUtils.getBean(mapperClass);
// Method method = clazz.getMethod(methodName, Serializable.class);//Long.class
Object query = SpringUtils.getBean(mapperClass);
String methodName = actionLog.methodName();
List<Method> methodList = Arrays.stream(mapperClass.getMethods()).filter(method -> method.getName().equals(methodName)).collect(Collectors.toList());
Method method = methodList.get(0);//
Object[] argsz = new Object[]{businessId};
Object beforeEntity = method.invoke(serviceObj, argsz);
Object beforeEntity = null;
try {
beforeEntity = method.invoke(query, argsz);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
//比较两个对象字段变化
Map<String,String> changeMap = BeanCopyUtils.getChangedFields(beforeEntity,afterEntity);
Map<String,String> changeMap = BeanCompareUtils.getChangedFields(beforeEntity,afterEntity);
String before = null == changeMap.get("before")?null:changeMap.get("before");
String after = null == changeMap.get("after")?null:changeMap.get("after");
//保存更新前后信息
@ -111,22 +138,26 @@ public class ActionLogAspect {
actionLogMessage.setBusinessId(businessId);
}
Object result;
long startTime = System.currentTimeMillis();
try {
//执行方法
result = joinPoint.proceed(args);
} catch (Exception e) {
throw e;
}
// 记录结束时间
long endTime = System.currentTimeMillis();
// 计算响应时间
long responseTime = endTime - startTime;
actionLogMessage.setResponseTime(responseTime);
handleLog(joinPoint, actionLog, null, result,actionLogMessage);
return result;
}
/**
* ==========================================以下方法无需修改===============================================
*/
/**
* 排除敏感属性字段
*/
@ -135,17 +166,7 @@ public class ActionLogAspect {
protected void handleLog(final JoinPoint joinPoint, ActionLog actionLog, final Exception e,
Object jsonResult,ActionLogMessage actionLogMessage) {
try {
actionLogMessage.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址
String ip = ServletUtils.getClientIP();
actionLogMessage.setOperIp(ip);
actionLogMessage.setOperUrl(StringUtils.sub(ServletUtils.getRequest().getRequestURI(), 0, 255));
//从业务系统获取传入
actionLogMessage.setOperUserId("2322434253");
actionLogMessage.setOperUserName("演示用户");
if (e != null) {
actionLogMessage.setStatus(BusinessStatus.FAIL.ordinal());
actionLogMessage.setErrorMsg(StringUtils.sub(e.getMessage(), 0, 2000));
@ -158,7 +179,7 @@ public class ActionLogAspect {
actionLogMessage.setRequestMethod(ServletUtils.getRequest().getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, actionLog, actionLogMessage, jsonResult);
long tracerId = IdWorker.nextId();
long tracerId = IdWorker.nextIdWithAppId();
actionLogMessage.setLogId(tracerId);
actionLogMessage.setCreateTime(System.currentTimeMillis());
UdpSender.offerActionLogger(actionLogMessage);
@ -169,11 +190,6 @@ public class ActionLogAspect {
}
}
public static void main(String[] args) {
long l = IdWorker.nextId();
System.out.println((l+"").length());
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解

25
example/src/main/java/com/inscloudtech/alog/clientdemo/demo/controller/DemoUserController.java

@ -49,6 +49,18 @@ public class DemoUserController {
return new Response(demoUserService.page(page, Wrappers.query(DemoUser)));
}
/**
* 新增示例用户
* @param
* @return Response
*/
@ActionLog(businessName = "用户管理", businessType = BusinessType.INSERT)
@PostMapping
public Response save(@RequestBody DemoUser demoUser) {
return new Response(demoUserService.save(demoUser));
}
/**
* 通过id查询示例用户
@ -56,27 +68,18 @@ public class DemoUserController {
* @return Response
*/
@GetMapping("/{id}" )
@ActionLog(businessName = "用户管理", businessType = BusinessType.SELECT)
public Response getById(@PathVariable("id" ) Long id) {
return new Response(demoUserService.getById(id));
}
/**
* 新增示例用户
* @param
* @return Response
*/
@ActionLog(businessName = "用户管理", businessType = BusinessType.INSERT)
@PostMapping
public Response save(@RequestBody DemoUser demoUser) {
return new Response(demoUserService.save(demoUser));
}
/**
* 修改示例用户
* @param
* @return Response
*/
@ActionLog(businessName = "用户管理",mapperClass = DemoUserMapper.class, businessType = BusinessType.UPDATE)
@ActionLog(businessName = "用户管理",mapperClass = DemoUserMapper.class, businessType = BusinessType.UPDATE,entityClass = DemoUser.class)
@PutMapping
public Response updateById(@RequestBody DemoUser demoUser) {
return new Response(demoUserService.updateById(demoUser));

72
example/src/main/java/com/inscloudtech/alog/clientdemo/utils/BeanCompareUtils.java

@ -0,0 +1,72 @@
package com.inscloudtech.alog.clientdemo.utils;
import com.inscloudtech.alog.common.annotation.IgnoreRecordField;
import com.inscloudtech.alog.common.annotation.NeedRecordField;
import java.lang.reflect.Field;
import java.util.*;
/**
* bean字段值比较
*
* @author inscloudtech
*/
public class BeanCompareUtils {
/**
* 排除敏感属性字段
*/
public static final List<String> EXCLUDE_FIELDS = new ArrayList();
/**
* 获取变更内容
* @param oldBean 更改前的Bean
* @param newBean 更改后的Bean
* @param <T>
* @return
*/
public static <T> Map getChangedFields(T oldBean, T newBean){
Field[] fields = newBean.getClass().getDeclaredFields();
StringBuilder beforeBuilder = new StringBuilder();
StringBuilder afterBuilder = new StringBuilder();
for(Field field : fields) {
field.setAccessible(true);
String fieldName = field.getName();
if (field.isAnnotationPresent(IgnoreRecordField.class) || EXCLUDE_FIELDS.contains(fieldName)) {
continue;
}
try {
Object oldValue = field.get(oldBean);
Object newValue = field.get(newBean);
if(null != newValue && !Objects.equals(newValue, oldValue)) {
if (field.isAnnotationPresent(NeedRecordField.class)) {
String tempFieldName = field.getAnnotation(NeedRecordField.class).fieldName();
fieldName = StringUtils.isEmpty(tempFieldName)?fieldName:tempFieldName;
}
beforeBuilder.append(fieldName);
beforeBuilder.append(": 【更改前:");
beforeBuilder.append(oldValue);
beforeBuilder.append("】,");
afterBuilder.append(fieldName);
afterBuilder.append(": 【更改后:");
afterBuilder.append(newValue);
afterBuilder.append("】,");
}
} catch (Exception e) {
e.printStackTrace();
}
}
Map<String,String> result = new HashMap<>(2);
result.put("before",beforeBuilder.toString());
result.put("after",afterBuilder.toString());
return result;
}
}

111
example/src/main/java/com/inscloudtech/alog/clientdemo/utils/BeanCopyUtils.java

@ -1,111 +0,0 @@
package com.inscloudtech.alog.clientdemo.utils;
import com.inscloudtech.alog.common.annotation.NeedRecordField;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
/**
* bean深拷贝工具(基于 cglib 性能优异)
* <p>
* 重点 cglib 不支持 拷贝到链式对象
* 例如: 源对象 拷贝到 目标(链式对象)
* 请区分好`浅拷贝``深拷贝`再做使用
*
* @author inscloudtech
*/
public class BeanCopyUtils {
/**
* 获取变更内容
* @param oldBean 更改前的Bean
* @param newBean 更改后的Bean
* @param <T>
* @return
*/
public static <T> Map getChangedFields(T oldBean, T newBean){
Field[] fields = newBean.getClass().getDeclaredFields();
StringBuilder beforeBuilder = new StringBuilder();
StringBuilder afterBuilder = new StringBuilder();
String fieldName = "";
for(Field field : fields) {
field.setAccessible(true);
if (!field.isAnnotationPresent(NeedRecordField.class)) {
continue;
}
try {
Object oldValue = field.get(oldBean);
Object newValue = field.get(newBean);
if(null != newValue && !Objects.equals(newValue, oldValue)) {
fieldName = field.getAnnotation(NeedRecordField.class).fieldName();
beforeBuilder.append(fieldName);
beforeBuilder.append(": 【更改前:");
beforeBuilder.append(oldValue);
beforeBuilder.append("】,");
afterBuilder.append(fieldName);
afterBuilder.append(": 【更改后:");
afterBuilder.append(newValue);
afterBuilder.append("】,");
}
} catch (Exception e) {
e.printStackTrace();
}
}
Map<String,String> result = new HashMap<>(2);
result.put("before",beforeBuilder.toString());
result.put("after",afterBuilder.toString());
return result;
}
/**
* 利用反射通过get方法获取bean中字段fieldName的值
* @param bean
* @param fieldName
* @return
* @throws Exception
*/
public static Object getFieldValue(Object bean, String fieldName)
throws Exception {
StringBuffer result = new StringBuffer();
String methodName = result.append("get")
.append(fieldName.substring(0, 1).toUpperCase())
.append(fieldName.substring(1)).toString();
Object rObject = null;
Method method = null;
@SuppressWarnings("rawtypes")
Class[] classArr = new Class[0];
method = bean.getClass().getMethod(methodName, classArr);
rObject = method.invoke(bean, new Object[0]);
return rObject;
}
private static void setFieldValue(Object bean, String fieldName, Object value)
throws Exception {
StringBuffer result = new StringBuffer();
String methodName = result.append("set")
.append(fieldName.substring(0, 1).toUpperCase())
.append(fieldName.substring(1)).toString();
/**
* 利用发射调用bean.set方法将value设置到字段
*/
Class[] classArr = new Class[1];
classArr[0]="java.lang.String".getClass();
Method method=bean.getClass().getMethod(methodName,classArr);
method.invoke(bean,value);
}
}

1
example/src/main/java/com/inscloudtech/alog/clientdemo/utils/StringUtils.java

@ -3,7 +3,6 @@ package com.inscloudtech.alog.clientdemo.utils;
import org.springframework.util.AntPathMatcher;
import java.util.*;
import java.util.function.Function;

6
example/src/main/resources/application.yml

@ -27,8 +27,10 @@ spring:
#worker的地址
aLog-workers:
aLog:
appId: 4667442
workers:
- 127.0.0.1:39999

10
worker/pom.xml

@ -55,11 +55,11 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.baomidou</groupId>-->
<!-- <artifactId>mybatis-plus-boot-starter</artifactId>-->
<!-- <version>3.4.3</version>-->
<!-- </dependency>-->
</dependencies>
<build>

4
worker/src/main/java/com/inscloudtech/alog/worker/disruptor/TracerConsumer.java

@ -3,6 +3,7 @@ package com.inscloudtech.alog.worker.disruptor;
import com.inscloudtech.alog.common.constant.Constant;
import com.inscloudtech.alog.common.constant.LogTypeEnum;
import com.inscloudtech.alog.common.model.RunLogMessage;
import com.inscloudtech.alog.common.utils.FastJsonUtils;
import com.inscloudtech.alog.common.utils.ProtostuffUtils;
import com.inscloudtech.alog.common.utils.ZstdUtils;
import com.inscloudtech.alog.common.model.TracerBean;
@ -86,8 +87,9 @@ public class TracerConsumer implements WorkHandler<OneTracer> {
} else if (LogTypeEnum.SPAN.equals(tracerData.getType())){
dealFilterModel(tracerData.getTracerBeanList());
}else{
String s = FastJsonUtils.convertObjectToJSON(tracerData.getActionLogs());
// ACTION_LOG
System.out.println("tracerData.getActionLogs() = " + tracerData.getActionLogs());
logger.info("接收到行为日志{}",s);
}
}

12
worker/src/main/resources/application.yml

@ -9,12 +9,12 @@ queue:
server:
port: 8086
spring:
datasource:
driverClassName: ru.yandex.clickhouse.ClickHouseDriver
url: jdbc:clickhouse://192.168.3.20:8123/default
username: default
password:
#spring:
# datasource:
# driverClassName: ru.yandex.clickhouse.ClickHouseDriver
# url: jdbc:clickhouse://192.168.3.20:8123/default
# username: default
# password:
#ck信息,自行修改
clickhouse:

2
worker/src/main/resources/jlog.sql

@ -38,7 +38,7 @@ CREATE TABLE tracer_log (
CREATE TABLE action_log (
log_id Int64 COMMENT '日志主键',
app_id Int64 COMMENT '应用id',
app_id Int8 COMMENT '应用id',
title String COMMENT '分类名称',
method String COMMENT '方法名称',
request_method String COMMENT '请求方式',

Loading…
Cancel
Save