Commit 6588593b by 姚冰

添加sendDataFromFile接口及相关实体

1 parent f2de590c
......@@ -44,7 +44,7 @@
<dependency>
<groupId>com.viontech.keliu</groupId>
<artifactId>AlgApiClient</artifactId>
<version>6.0.6</version>
<version>6.1.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<artifactId>tomcat-websocket</artifactId>
......
......@@ -26,6 +26,7 @@ public class VionConfig {
private String targetUrl;
private String keliuImageUrl;
private String keliuImagePath;
private String featureUrl;
private static final ThreadPoolExecutor POOL_EXECUTOR = new ThreadPoolExecutor(20, 20, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>(10000), new ThreadPoolExecutor.CallerRunsPolicy());
}
......@@ -4,10 +4,12 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.viontech.keliu.websocket.AlgApiClient;
import com.viontech.label.core.base.DateConverter;
import com.viontech.label.tool.keliu.service.FileService;
import com.viontech.label.tool.keliu.service.LocalFileServiceImpl;
import com.viontech.label.tool.keliu.service.OnlineFileServiceImpl;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
......@@ -31,6 +33,9 @@ import java.util.Date;
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${vion.feature-url:}")
private String faceFeatureUrl;
@Bean
@Primary
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
......@@ -54,17 +59,23 @@ public class WebConfig implements WebMvcConfigurer {
@Bean("fileService")
@ConditionalOnProperty("vion.keliu-image-path")
@ConditionalOnProperty(value = "vion.keliu-image-path")
public FileService localFileService() {
return new LocalFileServiceImpl();
}
@Bean("fileService")
@ConditionalOnProperty("vion.keliu-image-url")
@ConditionalOnProperty(value = "vion.keliu-image-url")
public FileService onlineFileService() {
return new OnlineFileServiceImpl();
}
@Bean("algApiClientFeature")
@ConditionalOnProperty(value = "vion.feature-url")
public AlgApiClient algApiClientFeatureConfig() {
return new AlgApiClient(faceFeatureUrl);
}
@Configuration
public static class CorsConfig {
@Bean
......
......@@ -3,9 +3,11 @@ package com.viontech.label.tool.keliu.controller;
import com.viontech.keliu.util.DateUtil;
import com.viontech.keliu.util.JsonMessageUtil;
import com.viontech.label.tool.keliu.config.VionConfig;
import com.viontech.label.tool.keliu.model.BodyRecognition;
import com.viontech.label.tool.keliu.model.FaceRecognition;
import com.viontech.label.tool.keliu.repository.KeliuRepository;
import com.viontech.label.tool.keliu.service.FileService;
import com.viontech.label.tool.keliu.service.LocalFileServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.*;
......@@ -18,6 +20,10 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
......@@ -59,7 +65,7 @@ public class KeliuController {
public Object sendData(@RequestParam Date date, @RequestParam Long mallId, @RequestParam Long packId, @RequestParam(required = false) Long taskId,
@RequestParam(required = false, defaultValue = "-1,0,1") String direction,
@RequestParam(required = false, defaultValue = "0,1") String personType,
@RequestParam(required = false,value = "deviceSerialNum") String deviceSerialNum,
@RequestParam(required = false, value = "deviceSerialNum") String deviceSerialNum,
@RequestParam(required = false, value = "channelSerialNum") String channelSerialNum,
@RequestParam(required = false, value = "startTime") Date startTime,
@RequestParam(required = false, value = "endTime") Date endTime
......@@ -72,7 +78,7 @@ public class KeliuController {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(20, 20, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>(10000), new ThreadPoolExecutor.CallerRunsPolicy());
try {
AtomicLong count = new AtomicLong();
List<FaceRecognition> faceRecognitions = keliuRepository.getFaceRecognitionsByDateAndMallId(date, mallId, direction, deviceSerialNum,personType, channelSerialNum, startTime, endTime);
List<FaceRecognition> faceRecognitions = keliuRepository.getFaceRecognitionsByDateAndMallId(date, mallId, direction, deviceSerialNum, personType, channelSerialNum, startTime, endTime);
log.info("上传数据量:" + faceRecognitions.size());
for (FaceRecognition faceRecognition : faceRecognitions) {
Future<JsonMessageUtil.JsonMessage> submit = threadPoolExecutor.submit(() -> {
......@@ -149,4 +155,104 @@ public class KeliuController {
map.add("gateId", faceRecognition.getGateId());
return new HttpEntity<>(map, headers);
}
private HttpEntity<MultiValueMap<String, Object>> getBodyRequestEntity(BodyRecognition bodyRecognition, byte[] bodyPic, byte[] bodyFeature, Long packId, Long taskId, Date date) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
ByteArrayResource picResource = new ByteArrayResource(bodyPic) {
@Override
public String getFilename() {
return bodyRecognition.getBodyPic();
}
};
ByteArrayResource featureResource = new ByteArrayResource(bodyFeature) {
@Override
public String getFilename() {
return bodyRecognition.getBodyPic() + ".feature";
}
};
LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
map.add("pic", picResource);
map.add("feature", featureResource);
map.add("unid", bodyRecognition.getUnid());
map.add("personUnid", bodyRecognition.getPersonUnid());
map.add("packId", packId);
map.add("taskId", taskId);
map.add("countTime", DateUtil.format("yyyy-MM-dd HH:mm:ss", date));
map.add("direction", bodyRecognition.getDirection());
map.add("gateId", bodyRecognition.getGateId());
return new HttpEntity<>(map, headers);
}
@GetMapping("/sendDataFromFile")
public Object sendDataFromFile(@RequestParam Date date, @RequestParam String pkgName, @RequestParam Long packId, @RequestParam(required = false) Long taskId,
@RequestParam(required = false, defaultValue = "-1,0,1") String direction,
@RequestParam(required = false, defaultValue = "0,1") String personType,
@RequestParam(required = false, value = "deviceSerialNum") String deviceSerialNum
) {
List<Future<JsonMessageUtil.JsonMessage>> responses = new LinkedList<>();
if (SEND_DATA) {
return JsonMessageUtil.getErrorJsonMsg("有数据传输任务正在进行");
} else {
SEND_DATA = true;
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(20, 20, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>(10000), new ThreadPoolExecutor.CallerRunsPolicy());
try {
AtomicLong count = new AtomicLong();
// List<FaceRecognition> faceRecognitions = keliuRepository.getFaceRecognitionsByDateAndMallId(date, mallId, direction, deviceSerialNum,personType, channelSerialNum, startTime, endTime);
List<BodyRecognition> bodyRecognitions = fileService.getFaceRecognition(pkgName, direction);
if (bodyRecognitions == null) {
return JsonMessageUtil.getSuccessJsonMsg("success:0;failed:0");
}
log.info("上传数据量:" + bodyRecognitions.size());
for (BodyRecognition bodyRecognition : bodyRecognitions) {
Future<JsonMessageUtil.JsonMessage> submit = threadPoolExecutor.submit(() -> {
try {
byte[] bodyPic = bodyRecognition.getBodyPicture();
byte[] bodyFeature = bodyRecognition.getBodyFeature();
HttpEntity<MultiValueMap<String, Object>> requestEntity = getBodyRequestEntity(bodyRecognition, bodyPic, bodyFeature, packId, taskId, date);
ResponseEntity<JsonMessageUtil.JsonMessage> exchange = restTemplate.exchange(vionConfig.getTargetUrl(), HttpMethod.POST, requestEntity, JsonMessageUtil.JsonMessage.class);
JsonMessageUtil.JsonMessage body = exchange.getBody();
log.info("unid:{},msg:{},count:{}/{}", bodyRecognition.getUnid(), body.getMsg(), count.incrementAndGet(), bodyRecognitions.size());
return body;
} catch (Exception e) {
log.info("", e);
return null;
}
});
responses.add(submit);
}
} finally {
while (threadPoolExecutor.getActiveCount() > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
threadPoolExecutor.shutdown();
SEND_DATA = false;
}
}
long success = 0;
long failed = 0;
for (Future<JsonMessageUtil.JsonMessage> future : responses) {
try {
JsonMessageUtil.JsonMessage jsonMessage = future.get(20, TimeUnit.SECONDS);
if (jsonMessage == null || !jsonMessage.isSuccess()) {
failed++;
} else {
success++;
}
} catch (Exception e) {
e.printStackTrace();
}
}
return JsonMessageUtil.getSuccessJsonMsg("success:" + success + ";failed:" + failed);
}
}
package com.viontech.label.tool.keliu.model;
import lombok.Data;
@Data
public class BodyRecognition {
private String personUnid;
private String unid;
private String direction;
private String bodyPic;
private String gateId;
byte[] bodyFeature;
byte[] bodyPicture;
}
package com.viontech.label.tool.keliu.model;
import lombok.Data;
@Data
public class ImageInfo {
private String imageName;
private Integer headX;
private Integer headY;
private Integer footX;
private Integer footY;
private Integer quadrant;
private Integer lefttopX;
private Integer lefttopY;
}
package com.viontech.label.tool.keliu.model;
import lombok.Data;
@Data
public class ROI {
int x;
int y;
int w;
int h;
}
package com.viontech.label.tool.keliu.service;
import com.viontech.label.tool.keliu.model.BodyRecognition;
import java.util.List;
/**
* .
*
......@@ -10,4 +14,7 @@ public interface FileService {
byte[] getFile(String filePath);
List<String> getFileContent(String pkgName);
List<BodyRecognition> getFaceRecognition(String pkgName, String direction);
}
package com.viontech.label.tool.keliu.service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.viontech.keliu.exception.ParameterExceptin;
import com.viontech.keliu.model.*;
import com.viontech.keliu.util.DateUtil;
import com.viontech.keliu.util.FileUtil;
import com.viontech.keliu.websocket.AlgApiClient;
import com.viontech.label.tool.keliu.config.VionConfig;
import com.viontech.label.tool.keliu.model.BodyRecognition;
import com.viontech.label.tool.keliu.model.FaceRecognition;
import com.viontech.label.tool.keliu.model.ImageInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import javax.annotation.Resource;
import javax.websocket.DeploymentException;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.DoubleBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* .
......@@ -18,6 +46,16 @@ public class LocalFileServiceImpl implements FileService {
@Resource
private VionConfig vionConfig;
@Value("${vion.keliu.dir}")
private String baseDir;
@Resource
@Qualifier("algApiClientFeature")
private AlgApiClient algApiClientFeature;
@Resource
private ObjectMapper objectMapper;
public LocalFileServiceImpl() {
log.info("======================= LocalFileServiceImpl =======================");
}
......@@ -30,4 +68,309 @@ public class LocalFileServiceImpl implements FileService {
return null;
}
}
@Override
public List<BodyRecognition> getFaceRecognition(String pkgName, String direction) {
String path = baseDir + "/" + pkgName;
String targetFile = "text.txt";
File file = new File(path);
if (!file.exists()) {
log.info("路径不存在:{}", path);
return null;
}
File[] fs = file.listFiles();
List<BodyRecognition> recognitions = new ArrayList<>();
for (File f : fs) {
if (f.isDirectory()) {
continue;
}
if (!f.getName().endsWith(".tar.gz")) {
continue;
}
path = f.getPath().substring(0, f.getPath().lastIndexOf(".tar.gz"));
File tmpPath = new File(path);
if (!tmpPath.exists()) {
tmpPath.mkdirs();
}
if (!unzipFile(f.getPath(), path)) {
log.info("解压文件失败:{}", path);
continue;
}
log.info("解压文件成功:{}", path);
List<String> paths = readFileDirectory(path, targetFile);
paths.forEach( filepath -> {
List<BodyRecognition> list = getFileBodyRecognition(filepath, f.getName(), direction);
if (list != null) {
recognitions.addAll(list);
}
});
removeFiles(path);
}
return recognitions;
// try {
// CompletableFuture<AlgResult> responseFuture = algApiClientFaceFeature.getFaceFeatureBatch(images, "jpg", faceKeys, Collections.emptyList());
// AlgResult result = responseFuture.get(120, TimeUnit.SECONDS);
// //logger.info("unid:{},提取特征返回:{}", item.getUnid(), JSON.toJSONString(result));
// List<FaceFeature> faceFeatureArr = result.getFaceFeatureArr();
// FaceFeature faceFeature = null;
// //提取融合特征的时候会返回所有照片的特征和融合特征,融合特征在最后一位
// if (faceFeatureArr != null && faceFeatureArr.size() > 0) {
// faceFeature = faceFeatureArr.get(faceFeatureArr.size() - 1);
// }
// if (faceFeature == null || faceFeature.getFeature() == null || faceFeature.getFeature().length <= 0) {
// log.warn("{} 提取融合特征失败,无法获取到特征,{}", pkgName, JSON.toJSONString(result));
// return null;
// }
// // 特征提取成功 开始构建person
// buildPerson(item, faceFeature, null);
// // feature对象是用来存储特征文件的,不参与比对
// String facePic = item.getFacePic();
// String faceNameSubStr = facePic.substring(0, facePic.length() - 5);
// String faceNameF = faceNameSubStr + "F.jpg";
//
// Feature feature = new Feature();
// feature = buildFeature(feature, faceNameF, AlgApiClient.IMAGE_TYPE_FACE, null, item.getFace_score(), faceFeature.getFeature());
// String json = objectMapper.writeValueAsString(feature);
// featureStorage.setItem(item.getChannelSerialnum() + "/" + faceNameF, json);
// logger.info("提取图片 {} 人脸特征成功", faceNameF);
// item.setFaceFeature(faceNameF);//把匹配特征文件字段替换成融合特征。
//
// } catch (Exception e) {
// logger.error("unid {} {},人脸特征提取失败", item.getUnid(), item.getFacePic(), e);
// }
// return null;
}
private List<BodyRecognition> getFileBodyRecognition(String filepath, String pkgName, String direction) {
String fileContent = readFile(filepath);
File file = new File(filepath);
String imgPath = file.getParentFile().getAbsolutePath();
JSONObject object = JSON.parseObject(fileContent);
if (object.isEmpty() || !object.containsKey("image_info")) {
return null;
}
List<BodyRecognition> recognitions = new ArrayList<>();
List<ImageInfo> imageInfoList = JSONArray.parseArray(object.getString("image_info").toString(), ImageInfo.class);
// ArrayList<List<FaceKey>> faceKeys = new ArrayList<>();//存储人脸关键点
for (ImageInfo temp : imageInfoList) {
byte[] byteArrayItem = new byte[0];
try {
byteArrayItem = FileUtils.readFileToByteArray(new File(imgPath + "/" + temp.getImageName()));
} catch (IOException e) {
log.error("读取图片{}错误", temp.getImageName(), e);
}
if (byteArrayItem == null) {
log.warn("{} 图片不存在", temp.getImageName());
continue;
}
String image = Base64.encodeBase64String(byteArrayItem);
// images.add(image);
FaceKey faceKey = new FaceKey(temp.getHeadX(), temp.getHeadY());
List<FaceKey> keyPoint = new ArrayList<>();
// faceKeys.add(keyPoint);
Map options = new HashMap<>();
ROI body_roi = new ROI(0, temp.getQuadrant(), temp.getLefttopX(), temp.getLefttopY());
ROI face_roi = new ROI(temp.getHeadX(), temp.getHeadY(), temp.getFootX(), temp.getFootY());
options.put("body_roi", body_roi);
options.put("face_roi", face_roi);
String imageType = "body";
CompletableFuture<AlgResult> responseFuture = null;
BodyFeature bodyFeature = null;
try {
responseFuture = algApiClientFeature.getFeatureAndAttr(image, imageType, AlgApiClient.IMAGE_FORMAT_JPG, keyPoint, options);
AlgResult result = responseFuture.get(120, TimeUnit.SECONDS);
if (result == null) {
log.warn("unid {} 图片{}提取人体特征失败,跳过", pkgName, temp.getImageName());
continue;
}
// "errCode": 1 //1 说明是低质图片,不提特征;2 说明是外卖图片,特征正常提取;0 其他
if (0 == result.getSuccess() || 1 == result.getErrCode()) {
log.error("unid {} 提取特征失败,description:[{}],rid [{}]}", pkgName, result.getDescription(), result.getRid());
continue;
}
bodyFeature = result.getBodyFeature();
if (bodyFeature == null) {
log.warn("unid {} 图片{}提取人体特征失败,跳过", pkgName, temp.getImageName());
continue;
}
} catch (Exception e) {
log.error("特征提取异常", e);
}
Double[] featureArr = bodyFeature.getFeature();
if (featureArr == null || featureArr.length <= 0) {
log.warn("unid {} 图片 提取人体特征失败,跳过", temp.getImageName());
continue;
}
Feature feature = new Feature();
feature.setFilename(temp.getImageName());
// feature.setType("body");
feature.setType("body");
List<ROI> faceROIs = new ArrayList<>();
faceROIs.add(face_roi);
feature.setFace_roi(faceROIs);
List<ROI> bodyROIs = new ArrayList<>();
bodyROIs.add(body_roi);
feature.setBody_roi(bodyROIs);
feature.setFace_type(1);//先默认设置类型为人脸
List<Data> datas = feature.getDatas();
if (datas == null) {
datas = new ArrayList<>();
feature.setDatas(datas);
}
Data data = new Data();
data.setType("server");
data.setData(featureArr);
datas.add(data);
feature.setModifyTime(DateUtil.format("yyyy-MM-dd HH:mm:ss", new Date()));
String featureStr = null;
try {
featureStr = objectMapper.writeValueAsString(feature);
} catch (JsonProcessingException e) {
continue;
}
BodyRecognition recognition = new BodyRecognition();
recognition.setDirection(direction);
recognition.setBodyPic(temp.getImageName());
recognition.setBodyFeature(featureStr.getBytes());
recognition.setBodyPicture(byteArrayItem);
recognition.setUnid(UUID.randomUUID().toString());
recognition.setPersonUnid(temp.getImageName().substring(0,36));
recognitions.add(recognition);
}
return recognitions;
}
@Override
public List<String> getFileContent(String pkgName) {
String path = baseDir + "/" + pkgName;
String targetFile = "text.txt";
File file = new File(path);
File[] fs = file.listFiles();
for (File f : fs) {
if (f.isDirectory()) {
continue;
}
if (!f.getName().endsWith(".tar.gz")) {
continue;
}
if (!unzipFile(f.getPath(), path)) {
log.info("解压文件失败:{}", path);
return null;
}
log.info("解压文件成功:{}", path);
path = path.substring(0, path.lastIndexOf(".tar.gz"));
List<String> paths = readFileDirectory(path, targetFile);
}
List<String> files = new ArrayList<>();
return null;
}
private List<String> readFileDirectory(String filePath, String dstFile) {
File file = new File(filePath);
File[] fs = file.listFiles();
if (fs == null || fs.length == 0) {
log.warn("{}目录下没有文件", filePath);
return null;
}
List<String> info = new ArrayList<>();
for (File f : fs) {
if (f.isDirectory()) {
List<String> path = readFileDirectory(f.getAbsolutePath(), dstFile);
if (path != null && path.size() > 0) {
info.addAll(path);
}
}
if ((f.getName().equals(dstFile))) {
log.warn("正在读取{}文件", f);
try {
info.add(f.getPath());
if (info != null && !info.isEmpty()) {
break;
}
} catch (Exception e) {
log.error("searchFile failed", e);
}
}
}
return info;
}
private String readFile(String filepath) {
File file = new File(filepath);
if (!file.exists() || !file.isFile()) {
return null;
}
return FileUtil.readFileByChars(filepath);
}
private boolean unzipFile(String filename, String path) {
String cmd = "tar xvf " + filename + " -C " + path;
String result = callShellByExec(cmd);
if (result.contains("tar:") || result.contains("Error")) {
return false;
}
return true;
}
private boolean removeFiles(String filename) {
String cmd = "rm -rf " + filename;
String result = callShellByExec(cmd);
if (result.contains("rm:") || result.contains("Error")) {
return false;
}
return true;
}
/**
* 使用 exec 调用shell脚本
* @param shellString
*/
private String callShellByExec(String shellString) {
BufferedReader reader = null;
try {
Process process = Runtime.getRuntime().exec(shellString);
int exitValue = process.waitFor();
if (0 != exitValue) {
log.error("call shell failed. error code is :" + exitValue);
}
// 返回值
reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line = null;
StringBuilder sb = new StringBuilder();
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
} catch (Throwable e) {
log.error("call shell failed. " + e);
}
return "Error";
}
}
package com.viontech.label.tool.keliu.service;
import com.viontech.label.tool.keliu.config.VionConfig;
import com.viontech.label.tool.keliu.model.BodyRecognition;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.List;
/**
* .
......@@ -28,4 +30,14 @@ public class OnlineFileServiceImpl implements FileService {
public byte[] getFile(String filePath) {
return restTemplate.getForObject(vionConfig.getKeliuImageUrl() + filePath, byte[].class);
}
@Override
public List<String> getFileContent(String pkgName) {
return null;
}
@Override
public List<BodyRecognition> getFaceRecognition(String pkgName, String direction) {
return null;
}
}
......@@ -9,14 +9,15 @@ spring.datasource.password=cdmqYwBq9uAdvLJb
# redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=vionredis
spring.redis.password=
#temporary
logging.level.com.viontech.label.mapper=error
debug=false
vion.local-client=true
#vion.target-url=http://36.112.68.214:12100/reid/upload
vion.target-url=http://127.0.0.1:12100/reid/upload
vion.keliu-image-url=https://vion-retail.oss-cn-beijing.aliyuncs.com/
#vion.keliu-image-path=/jason/VVAS/jingmao/
#vion.keliu-image-url=https://vion-retail.oss-cn-beijing.aliyuncs.com/
vion.keliu-image-path=/jason/VVAS/jingmao/
vion.keliu.dir=D:/Work/data
vion.feature-url=http://182.92.177.43:18500
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!