feat&fix: 1.3.0

1.限定创建组和场景仅支持字母数字和下划线
2.优化reqId使用AtomicLong作为请求id
3.优化客户端请求模板
This commit is contained in:
www.byteblogs.com 2023-05-13 18:08:53 +08:00 committed by byteblogs168
parent ea40b0fc9b
commit 4916fad455
13 changed files with 247 additions and 135 deletions

View File

@ -0,0 +1,80 @@
package com.aizuda.easy.retry.client.core.client.netty;
import cn.hutool.core.util.IdUtil;
import com.aizuda.easy.retry.client.core.cache.GroupVersionCache;
import com.aizuda.easy.retry.client.core.config.EasyRetryProperties;
import com.aizuda.easy.retry.common.core.context.SpringContext;
import com.aizuda.easy.retry.common.core.enums.HeadersEnum;
import com.aizuda.easy.retry.common.core.log.LogUtils;
import com.aizuda.easy.retry.common.core.util.HostUtils;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;
/**
* @author www.byteblogs.com
* @date 2023-05-13
* @since 1.3.0
*/
@Slf4j
public class NettyChannel {
private static final String HOST_ID = IdUtil.simpleUUID();
private static final String HOST_IP = HostUtils.getIp();
private static Channel CHANNEL;
public static void setChannel(Channel channel) {
NettyChannel.CHANNEL = channel;
}
/**
* 发送数据
*
* @param method 请求方式
* @param url url地址
* @param body 请求的消息体
* @throws InterruptedException
*/
public static void send(HttpMethod method, String url, String body) throws InterruptedException {
if (Objects.isNull(CHANNEL)) {
LogUtils.info(log, "send message but channel is null url:[{}] method:[{}] body:[{}] ", url, method, body);
return;
}
// 配置HttpRequest的请求数据和一些配置信息
FullHttpRequest request = new DefaultFullHttpRequest(
HttpVersion.HTTP_1_1, method, url, Unpooled.wrappedBuffer(body.getBytes(StandardCharsets.UTF_8)));
ServerProperties serverProperties = SpringContext.CONTEXT.getBean(ServerProperties.class);
request.headers()
.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON)
//开启长连接
.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE)
//设置传递请求内容的长度
.set(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes())
.set(HeadersEnum.HOST_ID.getKey(), HOST_ID)
.set(HeadersEnum.HOST_IP.getKey(), HOST_IP)
.set(HeadersEnum.GROUP_NAME.getKey(), EasyRetryProperties.getGroup())
.set(HeadersEnum.CONTEXT_PATH.getKey(), Optional.ofNullable(serverProperties.getServlet().getContextPath()).orElse("/"))
.set(HeadersEnum.HOST_PORT.getKey(), Optional.ofNullable(serverProperties.getPort()).orElse(8080))
.set(HeadersEnum.VERSION.getKey(), GroupVersionCache.getVersion())
;
//发送数据
CHANNEL.writeAndFlush(request).sync();
}
}

View File

@ -25,11 +25,17 @@ import java.util.concurrent.TimeUnit;
public class NettyHttpClientHandler extends SimpleChannelInboundHandler<FullHttpResponse> {
private NettyClient client;
public NettyHttpClientHandler() {
private NettyHttpConnectClient nettyHttpConnectClient;
public NettyHttpClientHandler(NettyHttpConnectClient nettyHttpConnectClient) {
client = RequestBuilder.<NettyClient, NettyResult>newBuilder()
.client(NettyClient.class)
.callback(nettyResult -> LogUtils.info(log,"heartbeat check requestId:[{}]", nettyResult.getRequestId()))
.build();
this.nettyHttpConnectClient = nettyHttpConnectClient;
}
@Override
@ -53,13 +59,14 @@ public class NettyHttpClientHandler extends SimpleChannelInboundHandler<FullHttp
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
super.channelUnregistered(ctx);
LogUtils.debug(log, "channelUnregistered");
ctx.channel().eventLoop().schedule(() -> {
EasyRetryProperties easyRetryProperties = SpringContext.getBeanByType(EasyRetryProperties.class);
EasyRetryProperties.ServerConfig server = easyRetryProperties.getServer();
LogUtils.info(log, "Reconnecting to:" + server.getHost() + ":" + server.getPort());
NettyHttpConnectClient.connect();
try {
nettyHttpConnectClient.reconnect();
} catch (Exception e) {
LogUtils.error(log, "reconnect error ", e);
}
}, 10, TimeUnit.SECONDS);
@ -91,7 +98,7 @@ public class NettyHttpClientHandler extends SimpleChannelInboundHandler<FullHttp
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
LogUtils.error(log,"easy-retry netty_http client exception", cause);
LogUtils.error(log,"easy-retry netty-http client exception", cause);
super.exceptionCaught(ctx, cause);
}

View File

@ -1,15 +1,8 @@
package com.aizuda.easy.retry.client.core.client.netty;
import cn.hutool.core.util.IdUtil;
import com.aizuda.easy.retry.client.core.cache.GroupVersionCache;
import com.aizuda.easy.retry.client.core.config.EasyRetryProperties;
import com.aizuda.easy.retry.client.core.Lifecycle;
import com.aizuda.easy.retry.common.core.context.SpringContext;
import com.aizuda.easy.retry.common.core.enums.HeadersEnum;
import com.aizuda.easy.retry.common.core.log.LogUtils;
import com.aizuda.easy.retry.common.core.util.HostUtils;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
@ -18,34 +11,33 @@ import io.netty.handler.codec.http.*;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;
import java.net.ConnectException;
import java.nio.channels.ClosedChannelException;
import java.util.concurrent.TimeUnit;
/**
* Netty 客户端
*
* @author: www.byteblogs.com
* @date : 2022-03-07 18:24
* @since 1.0.0
*/
@Slf4j
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class NettyHttpConnectClient implements Lifecycle, ApplicationContextAware {
private static final String HOST_ID = IdUtil.simpleUUID();
private static final String HOST_IP = HostUtils.getIp();
private ApplicationContext applicationContext;
private static Channel channel;
private static NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
private static Bootstrap bootstrap = new Bootstrap();
private volatile Channel channel;
@Override
public void start() {
@ -64,96 +56,105 @@ public class NettyHttpConnectClient implements Lifecycle, ApplicationContextAwar
.addLast(new IdleStateHandler(0, 0, 30, TimeUnit.SECONDS))
.addLast(new HttpClientCodec())
.addLast(new HttpObjectAggregator(5 * 1024 * 1024))
.addLast(new NettyHttpClientHandler());
.addLast(new NettyHttpClientHandler(thisClient));
}
})
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
ChannelFuture channelFuture = bootstrap.connect().sync();
channel = channelFuture.channel();
// 开启连接服务端
connect();
} catch (Exception e) {
log.error("Client start exception", e);
}
}
/**
* 连接客户端
*
* @return
*/
public void connect() {
try {
ChannelFuture channelFuture = bootstrap.connect();
boolean notTimeout = channelFuture.awaitUninterruptibly(30, TimeUnit.SECONDS);
channel = channelFuture.channel();
if (notTimeout) {
// 连接成功
if (channel != null && channel.isActive()) {
log.info("netty client started {} connect to server", channel.localAddress());
NettyChannel.setChannel(getChannel());
return;
}
Throwable cause = channelFuture.cause();
if (cause != null) {
exceptionHandler(cause);
}
} else {
log.warn("connect remote host[{}] timeout {}s", channel.remoteAddress(), 30);
}
} catch (Exception e) {
exceptionHandler(e);
}
channel.close();
}
/**
* 重连
*/
public void reconnect() {
ChannelFuture channelFuture = bootstrap.connect();
channelFuture.addListener((ChannelFutureListener) future -> {
Throwable cause = future.cause();
if (cause != null) {
exceptionHandler(cause);
} else {
channel = channelFuture.channel();
if (channel != null && channel.isActive()) {
log.info("Netty client {} reconnect to server", channel.localAddress());
NettyChannel.setChannel(getChannel());
}
}
});
}
/**
* 连接失败处理
*
* @param cause
*/
private void exceptionHandler(Throwable cause) {
if (cause instanceof ConnectException) {
log.error("connect error:{}", cause.getMessage());
} else if (cause instanceof ClosedChannelException) {
log.error("connect error:{}", "client has destroy");
} else {
log.error("connect error:", cause);
}
}
@Override
public void close() {
nioEventLoopGroup.shutdownGracefully();
}
public static void connect(){
channel = bootstrap.connect().addListener((ChannelFutureListener) future -> {
if (future.cause() != null){
LogUtils.debug(log,"operationComplete", future.cause());
}
}).channel();
}
public static void send(HttpMethod method, String url, String body) throws InterruptedException {
if (Objects.isNull(channel)) {
LogUtils.debug(log,"channel is null");
return;
if (channel != null) {
channel.close();
}
// 配置HttpRequest的请求数据和一些配置信息
FullHttpRequest request = new DefaultFullHttpRequest(
HttpVersion.HTTP_1_0, method, url, Unpooled.wrappedBuffer(body.getBytes(StandardCharsets.UTF_8)));
ServerProperties serverProperties = SpringContext.CONTEXT.getBean(ServerProperties.class);
request.headers()
.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON)
//开启长连接
.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE)
//设置传递请求内容的长度
.set(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes())
.set(HeadersEnum.HOST_ID.getKey(), HOST_ID)
.set(HeadersEnum.HOST_IP.getKey(), HOST_IP)
.set(HeadersEnum.GROUP_NAME.getKey(), EasyRetryProperties.getGroup())
.set(HeadersEnum.CONTEXT_PATH.getKey(), Optional.ofNullable(serverProperties.getServlet().getContextPath()).orElse("/"))
.set(HeadersEnum.HOST_PORT.getKey(), Optional.ofNullable(serverProperties.getPort()).orElse(8080))
.set(HeadersEnum.VERSION.getKey(), GroupVersionCache.getVersion())
;
//发送数据
channel.writeAndFlush(request).sync();
}
public static void sendSync(HttpMethod method, String url, String body) throws InterruptedException {
if (Objects.isNull(channel)) {
LogUtils.debug(log,"channel is null");
return;
if (nioEventLoopGroup != null) {
nioEventLoopGroup.shutdownGracefully();
}
// 配置HttpRequest的请求数据和一些配置信息
FullHttpRequest request = new DefaultFullHttpRequest(
HttpVersion.HTTP_1_0, method, url, Unpooled.wrappedBuffer(body.getBytes(StandardCharsets.UTF_8)));
ServerProperties serverProperties = SpringContext.CONTEXT.getBean(ServerProperties.class);
request.headers()
.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON)
//开启长连接
.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE)
//设置传递请求内容的长度
.set(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes())
.set(HeadersEnum.HOST_ID.getKey(), HOST_ID)
.set(HeadersEnum.HOST_IP.getKey(), HOST_IP)
.set(HeadersEnum.GROUP_NAME.getKey(), EasyRetryProperties.getGroup())
.set(HeadersEnum.CONTEXT_PATH.getKey(), Optional.ofNullable(serverProperties.getServlet().getContextPath()).orElse("/"))
.set(HeadersEnum.HOST_PORT.getKey(), Optional.ofNullable(serverProperties.getPort()).orElse(8080))
.set(HeadersEnum.VERSION.getKey(), GroupVersionCache.getVersion())
;
//发送数据
channel.writeAndFlush(request).sync();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public Channel getChannel() {
return channel;
}
}

View File

@ -20,11 +20,11 @@ import java.util.function.Consumer;
@Slf4j
public class RpcContext {
private static final ConcurrentMap<String, CompletableFuture> COMPLETABLE_FUTURE = new ConcurrentHashMap<>();
private static final ConcurrentMap<Long, CompletableFuture> COMPLETABLE_FUTURE = new ConcurrentHashMap<>();
private static final ConcurrentMap<String, Consumer> CALLBACK_CONSUMER = new ConcurrentHashMap<>();
private static final ConcurrentMap<Long, Consumer> CALLBACK_CONSUMER = new ConcurrentHashMap<>();
public static void invoke(String requestId, NettyResult nettyResult) {
public static void invoke(Long requestId, NettyResult nettyResult) {
try {
// 同步请求同步返回
@ -41,7 +41,7 @@ public class RpcContext {
}
public static <R> void setCompletableFuture(String id, CompletableFuture<R> completableFuture, Consumer<R> callable) {
public static <R> void setCompletableFuture(long id, CompletableFuture<R> completableFuture, Consumer<R> callable) {
if (Objects.nonNull(completableFuture)) {
COMPLETABLE_FUTURE.put(id, completableFuture);
}
@ -52,19 +52,19 @@ public class RpcContext {
}
public static <R> void setCompletableFuture(String id, Consumer<R> callable) {
public static <R> void setCompletableFuture(Long id, Consumer<R> callable) {
setCompletableFuture(id, null, callable);
}
public static <R> void setCompletableFuture(String id, CompletableFuture<R> completableFuture) {
public static <R> void setCompletableFuture(Long id, CompletableFuture<R> completableFuture) {
setCompletableFuture(id, completableFuture, null);
}
public static CompletableFuture<Object> getCompletableFuture(String id) {
public static CompletableFuture<Object> getCompletableFuture(Long id) {
return COMPLETABLE_FUTURE.get(id);
}
public static Consumer getConsumer(String id) {
public static Consumer getConsumer(Long id) {
return CALLBACK_CONSUMER.get(id);
}
}

View File

@ -3,7 +3,7 @@ package com.aizuda.easy.retry.client.core.client.proxy;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.lang.Assert;
import com.aizuda.easy.retry.client.core.annotation.Mapping;
import com.aizuda.easy.retry.client.core.client.netty.NettyHttpConnectClient;
import com.aizuda.easy.retry.client.core.client.netty.NettyChannel;
import com.aizuda.easy.retry.client.core.client.netty.RpcContext;
import com.aizuda.easy.retry.client.core.exception.EasyRetryClientException;
import com.aizuda.easy.retry.common.core.log.LogUtils;
@ -28,10 +28,10 @@ import java.util.function.Consumer;
@Slf4j
public class ClientInvokeHandler<R> implements InvocationHandler {
private Consumer<R> consumer;
private boolean async;
private long timeout;
private TimeUnit unit;
private final Consumer<R> consumer;
private final boolean async;
private final long timeout;
private final TimeUnit unit;
public ClientInvokeHandler(boolean async, long timeout, TimeUnit unit, Consumer<R> consumer) {
this.consumer = consumer;
@ -46,26 +46,23 @@ public class ClientInvokeHandler<R> implements InvocationHandler {
Mapping annotation = method.getAnnotation(Mapping.class);
EasyRetryRequest easyRetryRequest = new EasyRetryRequest(args);
sw.start("request start " + easyRetryRequest.getRequestId());
sw.start("request start " + easyRetryRequest.getReqId());
CompletableFuture<R> completableFuture = null;
if (async) {
RpcContext.setCompletableFuture(easyRetryRequest.getRequestId(), consumer);
RpcContext.setCompletableFuture(easyRetryRequest.getReqId(), consumer);
} else {
completableFuture = new CompletableFuture<>();
RpcContext.setCompletableFuture(easyRetryRequest.getRequestId(), completableFuture);
RpcContext.setCompletableFuture(easyRetryRequest.getReqId(), completableFuture);
}
try {
NettyHttpConnectClient.send(HttpMethod.valueOf(annotation.method().name()), annotation.path(),
JsonUtil.toJsonString(easyRetryRequest));
} catch (Exception e) {
throw e;
NettyChannel.send(HttpMethod.valueOf(annotation.method().name()), annotation.path(), easyRetryRequest.toString());
} finally {
sw.stop();
}
LogUtils.info(log,"request complete requestId:[{}] 耗时:[{}ms]", easyRetryRequest.getRequestId(), sw.getTotalTimeMillis());
LogUtils.info(log,"request complete requestId:[{}] 耗时:[{}ms]", easyRetryRequest.getReqId(), sw.getTotalTimeMillis());
if (async) {
return null;
} else {

View File

@ -1,8 +1,10 @@
package com.aizuda.easy.retry.common.core.model;
import com.aizuda.easy.retry.common.core.util.JsonUtil;
import lombok.Data;
import java.util.UUID;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author www.byteblogs.com
@ -12,21 +14,26 @@ import java.util.UUID;
@Data
public class EasyRetryRequest {
private String requestId;
private static final AtomicLong REQUEST_ID = new AtomicLong(0);
private long reqId;
private Object[] args;
public EasyRetryRequest(Object... args) {
this.args = args;
this.requestId = generateRequestId();
this.reqId = newId();
}
private static long newId() {
return REQUEST_ID.getAndIncrement();
}
public EasyRetryRequest() {
}
public String generateRequestId() {
return UUID.randomUUID().toString();
@Override
public String toString() {
return JsonUtil.toJsonString(this);
}
}

View File

@ -9,9 +9,9 @@ import lombok.Data;
@Data
public class NettyResult extends Result<Object> {
private String requestId;
private long requestId;
public NettyResult(int status, String message, Object data, String requestId) {
public NettyResult(int status, String message, Object data, long requestId) {
super(status, message, data);
this.requestId = requestId;
}
@ -19,7 +19,7 @@ public class NettyResult extends Result<Object> {
public NettyResult() {
}
public NettyResult(Object data, String requestId) {
public NettyResult(Object data, long requestId) {
super(data);
this.requestId = requestId;
}

View File

@ -1,6 +1,7 @@
package com.aizuda.easy.retry.server.server.handler;
import cn.hutool.core.net.url.UrlQuery;
import com.aizuda.easy.retry.common.core.log.LogUtils;
import com.aizuda.easy.retry.server.support.handler.ClientRegisterHandler;
import com.aizuda.easy.retry.common.core.model.NettyResult;
import com.aizuda.easy.retry.common.core.model.EasyRetryRequest;
@ -36,8 +37,8 @@ public class BeatHttpRequestHandler extends GetHttpRequestHandler {
@Override
public String doHandler(String content, UrlQuery query, HttpHeaders headers) {
log.info("心跳检查 content:[{}]", query.toString());
LogUtils.info(log,"Beat check content:[{}]", content);
EasyRetryRequest retryRequest = JsonUtil.parseObject(content, EasyRetryRequest.class);
return JsonUtil.toJsonString(new NettyResult("PONG", retryRequest.getRequestId()));
return JsonUtil.toJsonString(new NettyResult("PONG", retryRequest.getReqId()));
}
}

View File

@ -44,6 +44,6 @@ public class ConfigHttpRequestHandler extends GetHttpRequestHandler {
EasyRetryRequest retryRequest = JsonUtil.parseObject(content, EasyRetryRequest.class);
String groupName = headers.get(HeadersEnum.GROUP_NAME.getKey());
ConfigDTO configDTO = configAccess.getConfigInfo(groupName);
return JsonUtil.toJsonString(new NettyResult(JsonUtil.toJsonString(configDTO), retryRequest.getRequestId()));
return JsonUtil.toJsonString(new NettyResult(JsonUtil.toJsonString(configDTO), retryRequest.getReqId()));
}
}

View File

@ -48,10 +48,10 @@ public class ReportRetryInfoHttpRequestHandler extends PostHttpRequestHandler {
Object[] args = retryRequest.getArgs();
Boolean aBoolean = retryService.batchReportRetry(JsonUtil.parseList(JsonUtil.toJsonString(args[0]), RetryTaskDTO.class));
return JsonUtil.toJsonString(new NettyResult(StatusEnum.YES.getStatus(), "批量上报重试数据处理成功", aBoolean, retryRequest.getRequestId()));
return JsonUtil.toJsonString(new NettyResult(StatusEnum.YES.getStatus(), "批量上报重试数据处理成功", aBoolean, retryRequest.getReqId()));
} catch (Exception e) {
LogUtils.error(log, "批量上报重试数据失败", e);
return JsonUtil.toJsonString(new NettyResult(StatusEnum.YES.getStatus(), e.getMessage(), null, retryRequest.getRequestId()));
return JsonUtil.toJsonString(new NettyResult(StatusEnum.YES.getStatus(), e.getMessage(), null, retryRequest.getReqId()));
}
}
}

View File

@ -102,6 +102,7 @@ export default {
message: res.message
})
this.$refs.notify.reset()
this.$router.go(-1)
}
})
}).catch(() => {

View File

@ -3,18 +3,21 @@
<a-row class="form-row" :gutter="16">
<a-col :lg="6" :md="12" :sm="24">
<a-form-item label="组名称">
<a-form-item hidden>
<a-input
placeholder="请输入组名称"
hidden
v-decorator="[
'id',
]" />
</a-form-item>
<a-form-item label="组名称">
<a-input
placeholder="请输入组名称"
:maxLength="64"
:disabled='this.id && this.id > 0'
v-decorator="[
'groupName',
{rules: [{ required: true, message: '请输入组名称', whitespace: true}]}
{rules: [{ required: true, message: '请输入组名称', whitespace: true},{required: true, max: 64, message: '最多支持64个字符'}, {validator: validate}]}
]" />
</a-form-item>
</a-col>
@ -49,6 +52,7 @@
<a-form-item label="描述">
<a-input
placeholder="请输入描述"
:maxLength="256"
v-decorator="[
'description',
{rules: [{ required: true, message: '请输入描述', whitespace: true}]}
@ -142,9 +146,9 @@ export default {
})
},
validate (rule, value, callback) {
const regex = /^user-(.*)$/
const regex = /^[A-Za-z0-9_]+$/
if (!regex.test(value)) {
callback(new Error('需要以 user- 开头'))
callback(new Error('仅支持数字字母下划线'))
}
callback()
},
@ -159,6 +163,7 @@ export default {
formData.groupStatus = formData.groupStatus.toString()
formData.routeKey = formData.routeKey.toString()
formData.idGeneratorMode = formData.idGeneratorMode.toString()
this.id = formData.id
console.log('formData', formData)
form.setFieldsValue(formData)

View File

@ -319,6 +319,19 @@ export default {
return
}
const regex = /^[A-Za-z0-9_]{1,64}$/
if (!regex.test(sceneName)) {
this.memberLoading = false
this.$message.error('场景名称: 仅支持长度为:1~64位字符.格式为:数字、字母、下划线。')
return
}
if (description.length > 256) {
this.memberLoading = false
this.$message.error('描述: 仅支持长度为:1~256位字符')
return
}
const target = this.formData.find(item => key === item.key)
if (!target) {
this.formData.push({