Merge branch 'USVT-116' into 'master'

USVT-116 Blog 表,添加一个 discourse_id 字段

See merge request usvisatrack/usvisatrack.api.service!12
This commit is contained in:
YuCheng Hu 2022-12-22 22:57:45 +00:00
commit ace860102d
13 changed files with 455 additions and 43 deletions

View File

@ -1,16 +1,20 @@
package com.northtecom.visatrack.api.controller.api;
import com.northtecom.visatrack.api.controller.vo.VisaCaseSearch;
import com.northtecom.visatrack.api.service.impl.BlogService;
import com.northtecom.visatrack.api.service.impl.VisaCaseService;
import com.redfin.sitemapgenerator.ChangeFreq;
import com.redfin.sitemapgenerator.WebSitemapGenerator;
import com.redfin.sitemapgenerator.WebSitemapUrl;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.commons.io.FileUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.io.IOException;
import java.util.Base64;
import java.net.MalformedURLException;
import java.util.Date;
/**
* The API related to website content.
@ -32,9 +36,12 @@ public class ContentController {
private final BlogService blogService;
private final VisaCaseService visaCaseService;
public ContentController(BlogService blogService) {
public ContentController(BlogService blogService, VisaCaseService visaCaseService) {
this.blogService = blogService;
this.visaCaseService = visaCaseService;
}
@ -43,27 +50,34 @@ public class ContentController {
*
* @return a File with Spring MVC
*/
@GetMapping("/sitemap")
@GetMapping("/blog")
@Operation(summary = "Get Sitemap xml file", description = "This API will get sitemap xml file")
public String querySitemap() {
public String querySitemap() throws MalformedURLException {
byte[] bt = null;
String btx = null;
VisaCaseSearch search = new VisaCaseSearch();
// search.setCheckStartDate(new Date().mi);
// 38553
WebSitemapGenerator wsg = new WebSitemapGenerator("https://www.usvisatrack.com/", new File("D:\\home\\"));
for (int i = 0; i < 39553; i++) {
WebSitemapUrl url = new WebSitemapUrl.Options("https://www.usvisatrack.com/visa/detail?id=" + i)
.lastMod(new Date()).priority(1.0).changeFreq(ChangeFreq.HOURLY).build();
wsg.addUrl(url);
}
wsg.write();
// bt = FileUtils.readFileToByteArray(new File("D:\\home\\sitemap.xml"));
btx = "<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n" +
"<sitemap>\n" +
"<loc>https://www.ossez.com/sitemap_recent.xml</loc>\n" +
"<lastmod>2022-12-15T05:44:41Z</lastmod>\n" +
"</sitemap>\n" +
"<sitemap>\n" +
"<loc>https://www.ossez.com/sitemap_1.xml</loc>\n" +
"<lastmod>2022-12-15T05:44:41Z</lastmod>\n" +
"</sitemap>\n" +
"<sitemap>\n" +
"<loc>https://www.ossez.com/sitemap_2.xml</loc>\n" +
"<lastmod>2022-12-12T15:49:36Z</lastmod>\n" +
"</sitemap>\n" +
"</sitemapindex>";
btx = "";
// return Base64.getEncoder().withoutPadding().encodeToString(bt);

View File

@ -7,9 +7,7 @@ import com.northtecom.visatrack.api.controller.vo.ImportReportRequest;
import com.northtecom.visatrack.api.controller.vo.VisaCrawlRequest;
import com.northtecom.visatrack.api.data.spec.DateRange;
import com.northtecom.visatrack.api.model.request.api.CheckeeSyncRequest;
import com.northtecom.visatrack.api.service.impl.CrawlService;
import com.northtecom.visatrack.api.service.impl.VisaCaseService;
import com.northtecom.visatrack.api.service.impl.VisaReportCheckeeService;
import com.northtecom.visatrack.api.service.impl.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
@ -50,17 +48,32 @@ public class CrawlController {
private final VisaReportCheckeeService visaReportCheckeeService;
private final VisaCaseService visaCaseService;
private final BlogService blogService;
private final ObjectMapper objectMapper;
@Autowired
public CrawlController(CrawlService crawlService, VisaReportCheckeeService visaReportCheckeeService,
VisaCaseService visaCaseService) {
VisaCaseService visaCaseService, BlogService blogService) {
this.crawlService = crawlService;
this.visaReportCheckeeService = visaReportCheckeeService;
this.visaCaseService = visaCaseService;
this.blogService = blogService;
this.objectMapper = new ObjectMapper();
}
@PostMapping("/blog/sync")
@Operation(summary = "Sync ", description = "同步所有数据")
public Boolean syncBlogByDateRange(@RequestBody CheckeeSyncRequest checkeeSyncRequest) {
try {
blogService.crawlDiscourseKB(null);
} catch (Exception e) {
throw new BaseException(Status.BAD_REQUEST, "Cannot Process Request");
}
return true;
}
@PostMapping("/checkee/sync")
@Operation(summary = "Sync ", description = "同步所有数据")
public Boolean syncByDateRange(@RequestBody CheckeeSyncRequest checkeeSyncRequest) {
@ -83,7 +96,7 @@ public class CrawlController {
startDate = endDate.minus(p);
}
this.visaReportCheckeeService.syncDataAndReport(startDate,endDate);
this.visaReportCheckeeService.syncDataAndReport(startDate, endDate);
} catch (Exception e) {
throw new BaseException(Status.BAD_REQUEST, "Cannot Process Request");
@ -98,7 +111,7 @@ public class CrawlController {
public Boolean syncAllVisaDataFromCheckee() {
LocalDate endDate = LocalDate.now();
LocalDate startDate = LocalDate.of(2018, 10, 1);
this.visaReportCheckeeService.syncDataAndReport(startDate,endDate);
this.visaReportCheckeeService.syncDataAndReport(startDate, endDate);
return true;
}
@ -107,7 +120,7 @@ public class CrawlController {
public Boolean syncLast3monthVisaDataFromCheckee() {
LocalDate endDate = LocalDate.now();
LocalDate startDate = endDate.minusMonths(3);
this.visaReportCheckeeService.syncDataAndReport(startDate,endDate);
this.visaReportCheckeeService.syncDataAndReport(startDate, endDate);
return true;
}
@ -116,7 +129,7 @@ public class CrawlController {
public Boolean syncLast3YearsVisaDataFromCheckee() {
LocalDate endDate = LocalDate.now();
LocalDate startDate = endDate.minusYears(3);
this.visaReportCheckeeService.syncDataAndReport(startDate,endDate);
this.visaReportCheckeeService.syncDataAndReport(startDate, endDate);
return true;
}

View File

@ -2,12 +2,10 @@ package com.northtecom.visatrack.api.data.entity;
import com.northtecom.visatrack.api.base.data.BaseEntity;
import lombok.Data;
import org.checkerframework.common.aliasing.qual.Unique;
import org.hibernate.annotations.Type;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Index;
import javax.persistence.Table;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
@ -18,19 +16,23 @@ import java.util.List;
*/
@Entity
@Data
@Table(name = "blog", indexes = {
@Index(name = "ix_blog_category", columnList = "category"),
@Index(name = "ix_blog_title", columnList = "title"),
@Index(name = "ix_blog_summary", columnList = "summary"),
@Index(name = "ix_blog_author_name", columnList = "author_name"),
@Index(name = "ix_blog_publish_time", columnList = "publish_time")
})
@Table(name = "blog",
uniqueConstraints = {
@UniqueConstraint(columnNames = {"discourse_id"})},
indexes = {
@Index(name = "ix_blog_category", columnList = "category"),
@Index(name = "ix_blog_title", columnList = "title"),
@Index(name = "ix_blog_summary", columnList = "summary"),
@Index(name = "ix_blog_author_name", columnList = "author_name"),
@Index(name = "ix_blog_publish_time", columnList = "publish_time")})
@org.hibernate.annotations.Table(appliesTo = "blog", comment = "Blog")
public class Blog extends BaseEntity<Long> {
@Column(name = "author_name", columnDefinition = "varchar(50) COMMENT 'Author name'")
private String authorName;
@Column(name = "category", columnDefinition = "varchar(50) COMMENT 'Category'")
private String category;
@Column(name = "discourse_id", columnDefinition = "bigint COMMENT 'Discourse Id'")
private Long discourseId = 0L;
@Column(name = "title", columnDefinition = "varchar(300) COMMENT 'Title'")
private String blogTitle;
@Column(name = "content", columnDefinition = "LONGTEXT COMMENT 'Content'")

View File

@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import java.util.List;
import java.util.Set;
/**
*
@ -20,4 +21,7 @@ public interface BlogRepository extends PagingAndSortingRepository<Blog, Long>,
".blog b group by month_key",
nativeQuery = true)
List<Object[]> reportAllYear();
List<Blog> findAllByDiscourseIdIsIn(Set<Long> discourseIds);
}

View File

@ -0,0 +1,16 @@
package com.northtecom.visatrack.api.model.entity.discourse;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
@Data
@Accessors(chain = true)
public class DiscoursePost {
@JsonProperty(value = "cooked")
private String cooked;
}

View File

@ -0,0 +1,29 @@
package com.northtecom.visatrack.api.model.entity.discourse;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
@Data
@Accessors(chain = true)
public class DiscourseTopic {
private Long id;
@JsonProperty(required = true)
private String title;
@JsonProperty(value = "image_url")
private String imageUrl;
@JsonProperty(value = "excerpt")
private String excerpt;
@JsonProperty(value = "tags")
private List<String> tags;
}

View File

@ -1,20 +1,40 @@
package com.northtecom.visatrack.api.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.northtecom.visatrack.api.controller.vo.BlogCategoryReport;
import com.northtecom.visatrack.api.controller.vo.BlogYearReport;
import com.northtecom.visatrack.api.controller.vo.VisaCrawlRequest;
import com.northtecom.visatrack.api.data.entity.Blog;
import com.northtecom.visatrack.api.data.repository.BlogRepository;
import com.northtecom.visatrack.api.data.spec.BlogSpecification;
import com.northtecom.visatrack.api.model.entity.discourse.DiscoursePost;
import com.northtecom.visatrack.api.model.entity.discourse.DiscourseTopic;
import com.northtecom.visatrack.api.util.DiscourseUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.NumberUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Listing Service.
@ -25,13 +45,21 @@ import java.util.List;
@Service
@Slf4j
public class BlogService {
private final ObjectMapper om;
private final DiscourseUtils discourseUtils;
private final BlogRepository blogRepository;
@Autowired
public BlogService(BlogRepository blogRepository) {
public BlogService(DiscourseUtils discourseUtils, ObjectMapper om, BlogRepository blogRepository) {
this.discourseUtils = discourseUtils;
this.blogRepository = blogRepository;
om.disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES);
om.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
this.om = om;
}
public Page<Blog> queryAllPageBlog(Pageable pageable, String category, String year, String search) {
@ -81,6 +109,98 @@ public class BlogService {
return blogRepository.findById(blogId).orElse(null);
}
public void crawlDiscourseKB(VisaCrawlRequest visaCrawlRequest) {
String xx = discourseUtils.getTopicsList();
List<Blog> blogList = Lists.newArrayList();
try {
JsonNode topicsNode = om.readTree(xx).get("topic_list").get("topics");
List<DiscourseTopic> discourseTopics = om.readerFor(new TypeReference<List<DiscourseTopic>>() {
}).readValue(topicsNode);
Set<Long> discourseIds = discourseTopics.stream().map(DiscourseTopic::getId).collect(Collectors.toSet());
Map<Long, Blog> blogDBMap = blogRepository.findAllByDiscourseIdIsIn(discourseIds).stream().collect(Collectors.toMap(Blog::getDiscourseId, Function.identity()));
Map<Long, String> topicsDetailMap = discourseUtils.getTopicsDetail(discourseIds);
for (DiscourseTopic discourseTopic : discourseTopics) {
Long discourseId = discourseTopic.getId();
Blog blog = null;
if (ObjectUtils.isEmpty(blogDBMap.get(discourseId))) {
blog = new Blog();
blog.setDiscourseId(discourseId);
blog.setCategory("KB");
blog.setVisitCount(0);
} else {
blog = blogDBMap.get(discourseId);
}
blog.setAuthorName("USVisaTrack");
blog.setBlogTitle(discourseTopic.getTitle());
blog.setBlogCover(discourseTopic.getImageUrl());
blog.setBlogSummary(discourseTopic.getExcerpt());
blog.setBlogContent(discourseTopic.getExcerpt());
blog.setCategory("KB");
blog.setTags(discourseTopic.getTags());
blog.setPublishDatetime(LocalDateTime.now());
// SET CONTENT
DiscoursePost discoursePost = getx(om, topicsDetailMap, discourseId);
if (StringUtils.hasText(discoursePost.getCooked())) {
blog.setBlogContent(discoursePost.getCooked());
}
// SET VISIT COUNT
if (ObjectUtils.isEmpty(blog.getVisitCount())) {
blog.setVisitCount(0);
}
blogList.add(blog);
}
log.debug(">>>>>>>>>>>>> {}", blogList.size());
blogRepository.saveAll(blogList);
//
// List<DiscourseTopic> discourseTopics = om.co(topicsNode, DiscourseTopic.class);
} catch (JsonProcessingException e) {
log.error("s", e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private DiscoursePost getx(ObjectMapper om, Map<Long, String> topicsDetailMap, Long topicId) throws JsonProcessingException {
DiscoursePost discoursePost = new DiscoursePost();
String postStr = topicsDetailMap.get(topicId);
try {
if (StringUtils.hasLength(postStr)) {
JsonNode postsNode = om.readTree(postStr).get("post_stream").get("posts");
List<DiscoursePost> discoursePosts = om.readerFor(new TypeReference<List<DiscoursePost>>() {
}).readValue(postsNode);
if (CollectionUtils.isNotEmpty(discoursePosts)) {
discoursePost = discoursePosts.get(0);
}
}
return discoursePost;
} catch (JsonProcessingException e) {
log.error("s", e);
} catch (IOException e) {
throw new RuntimeException(e);
}
return null;
}
@Transactional
public void updateBlogVisitCount(Blog blog) {

View File

@ -1,6 +1,6 @@
package com.northtecom.visatrack.api.service.impl;
import com.northtecom.visatrack.api.base.util.EmailUtils;
import com.northtecom.visatrack.api.util.EmailUtils;
import com.northtecom.visatrack.api.data.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

View File

@ -0,0 +1,75 @@
package com.northtecom.visatrack.api.util;
import cn.hutool.core.lang.hash.Hash;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement;
import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagementClientBuilder;
import com.amazonaws.services.simplesystemsmanagement.model.GetParameterRequest;
import com.amazonaws.services.simplesystemsmanagement.model.GetParametersByPathRequest;
import com.amazonaws.services.simplesystemsmanagement.model.GetParametersByPathResult;
import com.amazonaws.services.simplesystemsmanagement.model.Parameter;
import com.google.common.collect.Maps;
import com.mailgun.api.v3.MailgunMessagesApi;
import com.mailgun.client.MailgunClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* Created with IntelliJ IDEA.
*
* @Author: XieYang
* @Date: 2022/10/30/7:24
* @Description:
*/
@Slf4j
@Component
public class AwsUtils {
private final AWSSimpleSystemsManagement ssmClient;
@Autowired
public AwsUtils(AWSSimpleSystemsManagement ssmClient) {
this.ssmClient = ssmClient;
}
public Map<String, String> getDiscourseConfigValueFromAWS(Map<String, String> discourseApiConfMap) {
Map<String, Parameter> awsKVMap = new HashMap<>();
try {
GetParametersByPathRequest request = new GetParametersByPathRequest();
request.setWithDecryption(false);
request.setRecursive(true);
request.setPath(DiscourseUtils.DISCOURSE_PATH);
GetParametersByPathResult result = this.ssmClient.getParametersByPath(request);
awsKVMap = Maps.uniqueIndex(result.getParameters(), Parameter::getName);
} catch (Exception ex) {
log.error("Get AWS Value for Key - [{}] Error", DiscourseUtils.DISCOURSE_PATH, ex);
}
// Update the map
for (Map.Entry<String, String> entry : discourseApiConfMap.entrySet()) {
discourseApiConfMap.put(entry.getKey(), awsKVMap.get(DiscourseUtils.DISCOURSE_PATH + "/" + entry.getKey()).getValue());
}
return discourseApiConfMap;
}
}

View File

@ -0,0 +1,123 @@
/*
* XMLUtils.java
*
* Created on December 6, 2001, 1:21 PM
*/
package com.northtecom.visatrack.api.util;
import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement;
import com.amazonaws.services.simplesystemsmanagement.model.GetParametersByPathRequest;
import com.mailgun.api.v3.MailgunMessagesApi;
import com.mailgun.model.message.Message;
import com.mailgun.model.message.MessageResponse;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Utilities for Email sending
*
* @author YuCheng Hu
*/
@Slf4j
@Component
public class DiscourseUtils {
public static final String DISCOURSE_PATH = "/discourse";
public static final String DISCOURSE_API_KEY = "api_key";
public static final String DISCOURSE_API_USERNAME = "api_username";
private Map<String, String> discourseApiConfMap = new HashMap<String, String>();
@Autowired
public DiscourseUtils(AwsUtils awsUtils) {
discourseApiConfMap.put(DISCOURSE_API_KEY, StringUtils.EMPTY);
discourseApiConfMap.put(DISCOURSE_API_USERNAME, StringUtils.EMPTY);
discourseApiConfMap = awsUtils.getDiscourseConfigValueFromAWS(discourseApiConfMap);
}
/**
* Send Test Email to check config and email sending API
*
* @return
*/
public String getTopicsList() {
OkHttpClient client = new OkHttpClient();
String responseStr;
try {
HttpUrl.Builder urlBuilder = HttpUrl.parse("https://www.visafn.com/c/kb/6.json").newBuilder();
//
// urlBuilder.addQueryParameter("appid", weChatAppId);
// urlBuilder.addQueryParameter("secret", weChatSecret);
// urlBuilder.addQueryParameter("code", weChatCode);
// urlBuilder.addQueryParameter("grant_type", "authorization_code");
Request request = new Request.Builder().url(urlBuilder.build().toString())
.addHeader(DISCOURSE_API_KEY, discourseApiConfMap.get(DISCOURSE_API_KEY))
.addHeader(DISCOURSE_API_USERNAME, discourseApiConfMap.get(DISCOURSE_API_USERNAME))
.build();
Call call = client.newCall(request);
Response response = call.execute();
responseStr = response.body().string();
} catch (IOException e) {
throw new RuntimeException(e);
}
return responseStr;
}
public Map<Long, String> getTopicsDetail(Set<Long> discourseTopics) {
Map<Long, String> topicMap = new HashMap<>();
OkHttpClient client = new OkHttpClient();
try {
for (Long discourseTopic : discourseTopics) {
String responseStr = StringUtils.EMPTY;
String urlStr = "https://www.visafn.com/t/" + discourseTopic + ".json";
HttpUrl.Builder urlBuilder = HttpUrl.parse(urlStr).newBuilder();
Request request = new Request.Builder().url(urlBuilder.build().toString())
.addHeader(DISCOURSE_API_KEY, discourseApiConfMap.get(DISCOURSE_API_KEY))
.addHeader(DISCOURSE_API_USERNAME, discourseApiConfMap.get(DISCOURSE_API_USERNAME))
.build();
Call call = client.newCall(request);
Response response = call.execute();
responseStr = response.body().string();
topicMap.put(discourseTopic, responseStr);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return topicMap;
}
}

View File

@ -3,7 +3,7 @@
*
* Created on December 6, 2001, 1:21 PM
*/
package com.northtecom.visatrack.api.base.util;
package com.northtecom.visatrack.api.util;
import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement;
import com.amazonaws.services.simplesystemsmanagement.model.GetParameterRequest;

View File

@ -4,6 +4,8 @@ package com.northtecom.visatrack.api;
import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement;
import com.amazonaws.services.simplesystemsmanagement.model.GetParameterRequest;
import com.amazonaws.services.simplesystemsmanagement.model.GetParameterResult;
import com.amazonaws.services.simplesystemsmanagement.model.GetParametersByPathRequest;
import com.amazonaws.services.simplesystemsmanagement.model.GetParametersByPathResult;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@ -50,4 +52,18 @@ class CloudTest {
}
@Test
public void testGetParameterStoreByPath() {
GetParametersByPathRequest request = new GetParametersByPathRequest();
request.setWithDecryption(false);
request.setRecursive(true);
request.setPath("/discourse");
GetParametersByPathResult result = ssmClient.getParametersByPath(request);
System.out.println(result.getNextToken());
}
}

View File

@ -1,6 +1,6 @@
package com.northtecom.visatrack.api;
import com.northtecom.visatrack.api.base.util.EmailUtils;
import com.northtecom.visatrack.api.util.EmailUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;