http接口封装基础调通
This commit is contained in:
@@ -25,6 +25,8 @@ package fun.asgc.neutrino.core.util;
|
|||||||
import fun.asgc.neutrino.core.annotation.Component;
|
import fun.asgc.neutrino.core.annotation.Component;
|
||||||
import fun.asgc.neutrino.core.bean.BeanFactory;
|
import fun.asgc.neutrino.core.bean.BeanFactory;
|
||||||
import fun.asgc.neutrino.core.bean.BeanFactoryAware;
|
import fun.asgc.neutrino.core.bean.BeanFactoryAware;
|
||||||
|
import fun.asgc.neutrino.core.bean.BeanIdentity;
|
||||||
|
import fun.asgc.neutrino.core.exception.BeanException;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -58,6 +60,20 @@ public class BeanManager implements BeanFactoryAware {
|
|||||||
return beanFactory.getBean(clazz);
|
return beanFactory.getBean(clazz);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试根据Bean身份标识获取bean
|
||||||
|
* @param identity
|
||||||
|
* @param <T>
|
||||||
|
* @return
|
||||||
|
* @throws BeanException
|
||||||
|
*/
|
||||||
|
public static <T> T getBean(BeanIdentity identity) {
|
||||||
|
if (null == beanFactory) {
|
||||||
|
throw new RuntimeException("BeanFactory尚未初始化");
|
||||||
|
}
|
||||||
|
return beanFactory.getBean(identity);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据超类获取bean集合
|
* 根据超类获取bean集合
|
||||||
* @param superClass
|
* @param superClass
|
||||||
|
|||||||
@@ -21,19 +21,30 @@
|
|||||||
*/
|
*/
|
||||||
package fun.asgc.neutrino.core.web;
|
package fun.asgc.neutrino.core.web;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import fun.asgc.neutrino.core.annotation.Autowired;
|
import fun.asgc.neutrino.core.annotation.Autowired;
|
||||||
import fun.asgc.neutrino.core.annotation.Component;
|
import fun.asgc.neutrino.core.annotation.Component;
|
||||||
import fun.asgc.neutrino.core.annotation.NonIntercept;
|
import fun.asgc.neutrino.core.annotation.NonIntercept;
|
||||||
import fun.asgc.neutrino.core.context.ApplicationConfig;
|
import fun.asgc.neutrino.core.context.ApplicationConfig;
|
||||||
|
import fun.asgc.neutrino.core.util.*;
|
||||||
import fun.asgc.neutrino.core.web.router.DefaultHttpRouter;
|
import fun.asgc.neutrino.core.web.router.DefaultHttpRouter;
|
||||||
|
import fun.asgc.neutrino.core.web.router.HttpRouteParam;
|
||||||
|
import fun.asgc.neutrino.core.web.router.HttpRouteResult;
|
||||||
|
import fun.asgc.neutrino.core.web.router.HttpRouterType;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.ChannelFutureListener;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.handler.codec.http.FullHttpRequest;
|
import io.netty.handler.codec.http.*;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author: aoshiguchen
|
* @author: aoshiguchen
|
||||||
* @date: 2022/7/15
|
* @date: 2022/7/15
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@NonIntercept
|
@NonIntercept
|
||||||
@Component
|
@Component
|
||||||
public class HttpRequestHandler {
|
public class HttpRequestHandler {
|
||||||
@@ -41,10 +52,66 @@ public class HttpRequestHandler {
|
|||||||
private ApplicationConfig applicationConfig;
|
private ApplicationConfig applicationConfig;
|
||||||
@Autowired
|
@Autowired
|
||||||
private DefaultHttpRouter defaultHttpRouter;
|
private DefaultHttpRouter defaultHttpRouter;
|
||||||
|
private static volatile String httpContextPath;
|
||||||
|
|
||||||
public void handle(ChannelHandlerContext context, FullHttpRequest request) {
|
public void handle(ChannelHandlerContext context, FullHttpRequest request) {
|
||||||
// TODO
|
String routePath = getRoutePath(request.uri());
|
||||||
System.out.println("hello");
|
HttpMethod httpMethod = HttpMethod.of(request.method().name());
|
||||||
|
HttpRouteResult httpRouteResult = defaultHttpRouter.route(new HttpRouteParam().setMethod(httpMethod).setUrl(routePath));
|
||||||
|
if (null == httpRouteResult) {
|
||||||
|
HttpServerUtil.send404Response(context, request.uri());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (HttpRouterType.METHOD == httpRouteResult.getType()) {
|
||||||
|
try {
|
||||||
|
Object invokeResult = httpRouteResult.getMethod().invoke(httpRouteResult.getInstance());
|
||||||
|
String res = String.valueOf(invokeResult);
|
||||||
|
if (!TypeUtil.isNormalBasicType(invokeResult.getClass())) {
|
||||||
|
res = JSONObject.toJSONString(invokeResult);
|
||||||
|
}
|
||||||
|
FullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(res.getBytes()));
|
||||||
|
fullHttpResponse.headers().add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON);
|
||||||
|
context.writeAndFlush(fullHttpResponse).addListener(ChannelFutureListener.CLOSE);
|
||||||
|
return;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO
|
||||||
|
HttpServerUtil.send404Response(context, request.uri());
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getRoutePath(String url) {
|
||||||
|
String httpContextPath = getHttpContextPath();
|
||||||
|
if (StringUtil.isEmpty(httpContextPath)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
String res = url.substring(httpContextPath.length());
|
||||||
|
if (StringUtil.isEmpty(res)) {
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getHttpContextPath() {
|
||||||
|
return LockUtil.doubleCheckProcessForNoException(
|
||||||
|
() -> null == httpContextPath,
|
||||||
|
this,
|
||||||
|
() -> {
|
||||||
|
httpContextPath = applicationConfig.getHttp().getContextPath();
|
||||||
|
if (StringUtil.isEmpty(httpContextPath)) {
|
||||||
|
httpContextPath = "/";
|
||||||
|
}
|
||||||
|
if (!httpContextPath.startsWith("/")) {
|
||||||
|
httpContextPath = "/" + httpContextPath;
|
||||||
|
}
|
||||||
|
if (httpContextPath.endsWith("/")) {
|
||||||
|
httpContextPath = httpContextPath.substring(0, httpContextPath.length() - 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() -> httpContextPath
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+132
-2
@@ -21,14 +21,27 @@
|
|||||||
*/
|
*/
|
||||||
package fun.asgc.neutrino.core.web.router;
|
package fun.asgc.neutrino.core.web.router;
|
||||||
|
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
import fun.asgc.neutrino.core.annotation.*;
|
import fun.asgc.neutrino.core.annotation.*;
|
||||||
import fun.asgc.neutrino.core.bean.BeanWrapper;
|
import fun.asgc.neutrino.core.bean.BeanWrapper;
|
||||||
import fun.asgc.neutrino.core.bean.SimpleBeanFactory;
|
import fun.asgc.neutrino.core.bean.SimpleBeanFactory;
|
||||||
|
import fun.asgc.neutrino.core.util.ArrayUtil;
|
||||||
|
import fun.asgc.neutrino.core.util.CollectionUtil;
|
||||||
|
import fun.asgc.neutrino.core.util.ReflectUtil;
|
||||||
|
import fun.asgc.neutrino.core.web.HttpMethod;
|
||||||
|
import fun.asgc.neutrino.core.web.annotation.GetMapping;
|
||||||
|
import fun.asgc.neutrino.core.web.annotation.PostMapping;
|
||||||
|
import fun.asgc.neutrino.core.web.annotation.RequestMapping;
|
||||||
|
import fun.asgc.neutrino.core.web.annotation.RestController;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认的http路由器
|
* 默认的http路由器
|
||||||
@@ -50,7 +63,125 @@ public class DefaultHttpRouter implements HttpRouter {
|
|||||||
public void init() {
|
public void init() {
|
||||||
log.debug("Http路由器初始化...");
|
log.debug("Http路由器初始化...");
|
||||||
List<BeanWrapper> beanWrapperList = webApplicationBeanFactory.beanWrapperList();
|
List<BeanWrapper> beanWrapperList = webApplicationBeanFactory.beanWrapperList();
|
||||||
// TODO 路由初始化
|
beanWrapperList.forEach(beanWrapper -> {
|
||||||
|
RestController restController = beanWrapper.getType().getAnnotation(RestController.class);
|
||||||
|
if (null == restController) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<HttpMethod> methods = null;
|
||||||
|
Set<String> paths = null;
|
||||||
|
if (beanWrapper.getType().isAnnotationPresent(GetMapping.class)) {
|
||||||
|
GetMapping getMapping = beanWrapper.getType().getAnnotation(GetMapping.class);
|
||||||
|
methods = Sets.newHashSet(HttpMethod.GET);
|
||||||
|
if (ArrayUtil.notEmpty(getMapping.value())) {
|
||||||
|
paths = Stream.of(getMapping.value()).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
} else if (beanWrapper.getType().isAnnotationPresent(PostMapping.class)) {
|
||||||
|
PostMapping postMapping = beanWrapper.getType().getAnnotation(PostMapping.class);
|
||||||
|
methods = Sets.newHashSet(HttpMethod.POST);
|
||||||
|
if (ArrayUtil.notEmpty(postMapping.value())) {
|
||||||
|
paths = Stream.of(postMapping.value()).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
} else if (beanWrapper.getType().isAnnotationPresent(RequestMapping.class)) {
|
||||||
|
RequestMapping requestMapping = beanWrapper.getType().getAnnotation(RequestMapping.class);
|
||||||
|
if (ArrayUtil.notEmpty(requestMapping.value())) {
|
||||||
|
paths = Stream.of(requestMapping.value()).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
if (ArrayUtil.notEmpty(requestMapping.method())) {
|
||||||
|
methods = Stream.of(requestMapping.method()).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CollectionUtil.isEmpty(methods)){
|
||||||
|
methods = Stream.of(HttpMethod.values()).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
if (CollectionUtil.isEmpty(paths)) {
|
||||||
|
paths = Sets.newHashSet("");
|
||||||
|
}
|
||||||
|
paths = paths.stream().map(p -> {
|
||||||
|
if (p.endsWith("/")) {
|
||||||
|
return p.substring(0, p.length() - 1);
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}).collect(Collectors.toSet());
|
||||||
|
methodScan(beanWrapper, methods, paths);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void methodScan(BeanWrapper beanWrapper, Set<HttpMethod> httpMethods, Set<String> paths) {
|
||||||
|
Set<Method> methods = ReflectUtil.getDeclaredMethods(beanWrapper.getType());
|
||||||
|
methods.forEach(method -> {
|
||||||
|
Set<HttpMethod> realityHttpMethods = null;
|
||||||
|
Set<String> subPaths = null;
|
||||||
|
if (method.isAnnotationPresent(GetMapping.class)) {
|
||||||
|
GetMapping getMapping = method.getAnnotation(GetMapping.class);
|
||||||
|
if (!httpMethods.contains(HttpMethod.GET)) {
|
||||||
|
throw new RuntimeException(String.format("Controller[type:%s, name:%s] method:%s 方法上的GetMapping注解与类的声明发生冲突!", beanWrapper.getType().getName(), beanWrapper.getName(), method.toString()));
|
||||||
|
}
|
||||||
|
realityHttpMethods = Sets.newHashSet(HttpMethod.GET);
|
||||||
|
if (ArrayUtil.notEmpty(getMapping.value())) {
|
||||||
|
subPaths = Stream.of(getMapping.value()).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
} else if (method.isAnnotationPresent(PostMapping.class)) {
|
||||||
|
PostMapping postMapping = method.getAnnotation(PostMapping.class);
|
||||||
|
if (!httpMethods.contains(HttpMethod.POST)) {
|
||||||
|
throw new RuntimeException(String.format("Controller[type:%s, name:%s] method:%s 方法上的PostMapping注解与类的声明发生冲突!", beanWrapper.getType().getName(), beanWrapper.getName(), method.toString()));
|
||||||
|
}
|
||||||
|
realityHttpMethods = Sets.newHashSet(HttpMethod.POST);
|
||||||
|
if (ArrayUtil.notEmpty(postMapping.value())) {
|
||||||
|
subPaths = Stream.of(postMapping.value()).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
} else if (method.isAnnotationPresent(RequestMapping.class)) {
|
||||||
|
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
|
||||||
|
if (ArrayUtil.notEmpty(requestMapping.method())) {
|
||||||
|
realityHttpMethods = Stream.of(requestMapping.method()).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
if (!httpMethods.containsAll(realityHttpMethods)) {
|
||||||
|
throw new RuntimeException(String.format("Controller[type:%s, name:%s] method:%s 方法上的RequestMapping注解与类的声明发生冲突!", beanWrapper.getType().getName(), beanWrapper.getName(), method.toString()));
|
||||||
|
}
|
||||||
|
if (ArrayUtil.notEmpty(requestMapping.value())) {
|
||||||
|
subPaths = Stream.of(requestMapping.value()).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (CollectionUtil.isEmpty(realityHttpMethods)) {
|
||||||
|
realityHttpMethods = Sets.newHashSet(httpMethods);
|
||||||
|
}
|
||||||
|
if (CollectionUtil.isEmpty(subPaths)) {
|
||||||
|
subPaths = Sets.newHashSet("/");
|
||||||
|
}
|
||||||
|
subPaths = subPaths.stream().map(p -> {
|
||||||
|
if (!p.startsWith("/")) {
|
||||||
|
return "/" + p;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}).collect(Collectors.toSet());
|
||||||
|
addRoute(beanWrapper, method, realityHttpMethods, paths, subPaths);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addRoute(BeanWrapper beanWrapper, Method method, Set<HttpMethod> httpMethods, Set<String> paths, Set<String> subPaths) {
|
||||||
|
for (HttpMethod httpMethod : httpMethods) {
|
||||||
|
for (String path : paths) {
|
||||||
|
for (String subPath : subPaths) {
|
||||||
|
addRoute(beanWrapper, method, httpMethod, path + subPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void addRoute(BeanWrapper beanWrapper, Method method, HttpMethod httpMethod, String path) {
|
||||||
|
log.info("addRoute[{}#{}] httpMethod:{} path:{}", beanWrapper.getType().getName(), method.getName(), httpMethod.name(), path);
|
||||||
|
HttpRouteIdentity identity = new HttpRouteIdentity(httpMethod, path);
|
||||||
|
if (routeCache.containsKey(identity)) {
|
||||||
|
throw new RuntimeException(String.format("Controller[type:%s, name:%s] method:%s 路由存在重复!", beanWrapper.getType().getName(), beanWrapper.getName(), method.getName()));
|
||||||
|
}
|
||||||
|
routeCache.put(identity, new HttpRouteInfo()
|
||||||
|
.setType(HttpRouterType.METHOD)
|
||||||
|
.setMethod(method)
|
||||||
|
.setBeanIdentity(beanWrapper.getIdentity())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -75,5 +206,4 @@ public class DefaultHttpRouter implements HttpRouter {
|
|||||||
public void destroy() {
|
public void destroy() {
|
||||||
log.debug("Http路由器销毁...");
|
log.debug("Http路由器销毁...");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -55,7 +55,7 @@ public class HttpRouteIdentity implements Identity {
|
|||||||
Assert.notEmpty(url, "url不能为空!");
|
Assert.notEmpty(url, "url不能为空!");
|
||||||
this.method = method;
|
this.method = method;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.identityHashCode = System.identityHashCode(url);
|
this.identityHashCode = System.identityHashCode(method);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpMethod getMethod() {
|
public HttpMethod getMethod() {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ package fun.asgc.neutrino.core.web.router;
|
|||||||
|
|
||||||
import fun.asgc.neutrino.core.bean.BeanIdentity;
|
import fun.asgc.neutrino.core.bean.BeanIdentity;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ import java.lang.reflect.Method;
|
|||||||
* @author: aoshiguchen
|
* @author: aoshiguchen
|
||||||
* @date: 2022/7/16
|
* @date: 2022/7/16
|
||||||
*/
|
*/
|
||||||
|
@Accessors(chain = true)
|
||||||
@Data
|
@Data
|
||||||
public class HttpRouteInfo {
|
public class HttpRouteInfo {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,12 +23,14 @@ package fun.asgc.neutrino.core.web.router;
|
|||||||
|
|
||||||
import fun.asgc.neutrino.core.web.HttpMethod;
|
import fun.asgc.neutrino.core.web.HttpMethod;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* http路由参数
|
* http路由参数
|
||||||
* @author: aoshiguchen
|
* @author: aoshiguchen
|
||||||
* @date: 2022/7/16
|
* @date: 2022/7/16
|
||||||
*/
|
*/
|
||||||
|
@Accessors(chain = true)
|
||||||
@Data
|
@Data
|
||||||
public class HttpRouteParam {
|
public class HttpRouteParam {
|
||||||
private HttpMethod method;
|
private HttpMethod method;
|
||||||
|
|||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 aoshiguchen
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
package fun.asgc.neutrino.proxy.server.controller;
|
||||||
|
|
||||||
|
import fun.asgc.neutrino.core.web.annotation.GetMapping;
|
||||||
|
import fun.asgc.neutrino.core.web.annotation.RequestMapping;
|
||||||
|
import fun.asgc.neutrino.core.web.annotation.RestController;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author: aoshiguchen
|
||||||
|
* @date: 2022/7/17
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/test1")
|
||||||
|
public class Test1Controller {
|
||||||
|
private static final SimpleDateFormat SDF = new SimpleDateFormat( "yyyy-MM-dd :HH:mm:ss");
|
||||||
|
|
||||||
|
@GetMapping("hello")
|
||||||
|
public String hello() {
|
||||||
|
return "hello 现在时间是:" + SDF.format(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user