feat(REQ-2972): 查询与新增接口基本实现

This commit is contained in:
chenwenjian 2024-09-11 18:32:22 +08:00
parent 3c225daaa5
commit edd2e69aa7
17 changed files with 829 additions and 2 deletions

View File

@ -0,0 +1,54 @@
package cn.axzo.log.platform.client.feign;
import cn.axzo.log.platform.client.model.req.LogAddReq;
import cn.axzo.log.platform.client.model.req.LogFindReq;
import cn.axzo.log.platform.client.model.resp.LogResp;
import cn.azxo.framework.common.model.CommonResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.util.List;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 10:31
*/
@FeignClient(name = "log-plat", url = "http://log-plat:8080")
public interface LogApi {
/**
* 通过筛查条件查询日志列表
* <p>
* 若所有条件为空则返回按时间序倒排的1000条日志
* </p>
*
* @param req 查询条件 {@link LogFindReq}
* @return 日志列表
*/
@PostMapping(value = "/api/log/findLogsWithExample", consumes = "application/json")
CommonResponse<List<LogResp>> findLogsWithExample(@RequestBody LogFindReq req);
/**
* 通过日志id查询日志详情
*
* @param id 日志id
* @return 日志
*/
@GetMapping(value = "/api/log/findLogById")
CommonResponse<LogResp> findLogById(@RequestParam("id") @NotBlank(message = "id is required") String id);
/**
* 添加日志
*
* @param req 日志信息 {@link LogAddReq}
* @return 日志id
*/
@PostMapping(value = "/api/log/addLog", consumes = "application/json")
CommonResponse<String> addLog(@RequestBody @Valid LogAddReq req);
}

View File

@ -0,0 +1,46 @@
package cn.axzo.log.platform.client.model.req;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import java.util.List;
/**
* 新增日志请求参数
*
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 10:51
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LogAddReq {
/**
* 场景
*/
@NotBlank(message = "scene is required")
private String scene;
/**
* 日志级别, DEBUG, INFO, WARN, ERROR
*/
@NotBlank(message = "level is required")
private String level;
/**
* 日志标签建议可以将服务名称操作等信息作为tag
*/
private List<String> tags;
/**
* 日志内容
*/
@NotBlank(message = "message is required")
private String message;
}

View File

@ -0,0 +1,52 @@
package cn.axzo.log.platform.client.model.req;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 查询日志列表请求参数
*
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 10:45
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LogFindReq {
/**
* 场景
*/
private String scene;
/**
* 日志级别, DEBUG, INFO, WARN, ERROR
*/
private String level;
/**
* 日志标签
*/
private List<String> tags;
/**
* 日志内容
*/
private String message;
/**
* 开始时间时间戳字符串
*/
private Long start;
/**
* 结束时间时间戳字符串
*/
private Long end;
}

View File

@ -0,0 +1,50 @@
package cn.axzo.log.platform.client.model.resp;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 10:38
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LogResp {
private String id;
/**
* 日志场景
*/
private String scene;
/**
* 日志级别, DEBUG, INFO, WARN, ERROR
*/
private String level;
/**
* 日志时间戳
*/
private Long timestamp;
/**
* 日志标签建议可以将服务名称操作等信息作为tag
*/
private List<String> tags;
/**
* 日志内容
*/
private String message;
public String getTimestampAsString() {
return String.valueOf(timestamp);
}
}

View File

@ -146,6 +146,29 @@
<groupId>cn.axzo.trade</groupId>
<artifactId>trade-data-security-sd-extension</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.platform</groupId>
<artifactId>axzo-log-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>5.0.2</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>5.2.4</version>
</dependency>
</dependencies>
<build>

View File

@ -19,7 +19,8 @@ import org.springframework.core.env.Environment;
"cn.axzo.apollo",
"cn.axzo.pudge",
"cn.axzo.elise",
"cn.axzo.basics"
"cn.axzo.basics",
"cn.axzo.log.platform.client"
})
@EnableDiscoveryClient
@SpringBootApplication(scanBasePackages = "cn.axzo.log.platform.server")

View File

@ -0,0 +1,41 @@
package cn.axzo.log.platform.server.controller.api;
import cn.axzo.log.platform.client.feign.LogApi;
import cn.axzo.log.platform.client.model.req.LogAddReq;
import cn.axzo.log.platform.client.model.req.LogFindReq;
import cn.axzo.log.platform.client.model.resp.LogResp;
import cn.axzo.log.platform.server.service.LogService;
import cn.azxo.framework.common.model.CommonResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 日志接口实现
*
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 10:55
*/
@RestController
@RequiredArgsConstructor
public class LogController implements LogApi {
private final LogService logService;
@Override
public CommonResponse<List<LogResp>> findLogsWithExample(LogFindReq req) {
return CommonResponse.success(logService.findLogsWithExample(req));
}
@Override
public CommonResponse<LogResp> findLogById(String id) {
return CommonResponse.success(logService.findLogById(id));
}
@Override
public CommonResponse<String> addLog(LogAddReq req) {
return CommonResponse.success(logService.addLog(req));
}
}

View File

@ -0,0 +1,67 @@
package cn.axzo.log.platform.server.controller.web;
import cn.axzo.log.platform.client.model.req.LogAddReq;
import cn.axzo.log.platform.client.model.req.LogFindReq;
import cn.axzo.log.platform.client.model.resp.LogResp;
import cn.axzo.log.platform.server.service.LogService;
import cn.azxo.framework.common.model.CommonResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.util.List;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 18:16
*/
@RestController
@RequestMapping("/webApi/log")
@RequiredArgsConstructor
public class WebLogController {
private final LogService logService;
/**
* 通过筛查条件查询日志列表
* <p>
* 若所有条件为空则返回按时间序倒排的1000条日志
* </p>
*
* @param req 查询条件 {@link LogFindReq}
* @return 日志列表
*/
@PostMapping(value = "/findLogsWithExample", consumes = "application/json")
CommonResponse<List<LogResp>> findLogsWithExample(@RequestBody LogFindReq req) {
return CommonResponse.success(logService.findLogsWithExample(req));
}
/**
* 通过日志id查询日志详情
*
* @param id 日志id
* @return 日志
*/
@GetMapping(value = "/findLogById")
CommonResponse<LogResp> findLogById(@RequestParam("id") @NotBlank(message = "id is required") String id) {
return CommonResponse.success(logService.findLogById(id));
}
/**
* 添加日志
*
* @param req 日志信息 {@link LogAddReq}
* @return 日志id
*/
@PostMapping(value = "/addLog", consumes = "application/json")
CommonResponse<String> addLog(@RequestBody @Valid LogAddReq req) {
return CommonResponse.success(logService.addLog(req));
}
}

View File

@ -0,0 +1,52 @@
package cn.axzo.log.platform.server.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.time.LocalDateTime;
import java.util.List;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/10 11:41
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "log")
public class LogEntity {
@Id
private String id;
/**
* 日志场景
*/
private String scene;
/**
* 日志级别, DEBUG, INFO, WARN, ERROR
*/
private String level;
/**
* 日志时间
*/
private Long timestamp;
/**
* 日志标签建议可以将服务名称操作等信息作为tag
*/
private List<String> tags;
/**
* 日志内容
*/
private String message;
}

View File

@ -0,0 +1,15 @@
package cn.axzo.log.platform.server.mapper;
import cn.axzo.log.platform.server.entity.LogEntity;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/10 13:56
*/
@Repository
public interface LogMapper extends MongoRepository<LogEntity, String> {
}

View File

@ -0,0 +1,57 @@
package cn.axzo.log.platform.server.resolvers;
import cn.axzo.log.platform.client.model.req.LogAddReq;
import cn.axzo.log.platform.client.model.req.LogFindReq;
import cn.axzo.log.platform.client.model.resp.LogResp;
import cn.axzo.log.platform.server.service.LogService;
import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/10 14:52
*/
@Component
@RequiredArgsConstructor
public class LogResolver implements GraphQLQueryResolver, GraphQLMutationResolver {
private final LogService logService;
/**
* 通过条件筛查日志
* <p>
* 若所有条件为空则返回按时间序倒排的1000条日志
* </p>
*
* @param req 查询条件 {@link LogFindReq}
* @return 按时间倒序排序的日志记录集合
*/
public List<LogResp> findLogsWithExample(LogFindReq req) {
return logService.findLogsWithExample(req);
}
/**
* 通过id查询日志
*
* @param id 日志id
* @return 日志记录
*/
public LogResp findLogById(String id) {
return logService.findLogById(id);
}
/**
* 添加日志
*
* @param req 日志
* @return 日志记录id
*/
public String addLog(LogAddReq req) {
return logService.addLog(req);
}
}

View File

@ -0,0 +1,21 @@
package cn.axzo.log.platform.server.service;
import cn.axzo.log.platform.client.model.req.LogAddReq;
import cn.axzo.log.platform.client.model.req.LogFindReq;
import cn.axzo.log.platform.client.model.resp.LogResp;
import java.util.List;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 11:02
*/
public interface LogService {
String addLog(LogAddReq req);
LogResp findLogById(String id);
List<LogResp> findLogsWithExample(LogFindReq req);
}

View File

@ -0,0 +1,173 @@
package cn.axzo.log.platform.server.service.impl;
import cn.axzo.basics.common.util.NumberUtil;
import cn.axzo.log.platform.client.model.req.LogAddReq;
import cn.axzo.log.platform.client.model.req.LogFindReq;
import cn.axzo.log.platform.client.model.resp.LogResp;
import cn.axzo.log.platform.server.entity.LogEntity;
import cn.axzo.log.platform.server.mapper.LogMapper;
import cn.axzo.log.platform.server.service.LogService;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 11:04
*/
@Service
@RequiredArgsConstructor
public class LogServiceImpl implements LogService {
private final LogMapper logMapper;
private final MongoTemplate mongoTemplate;
/**
* 添加日志
*
* @param req 日志信息 {@link LogAddReq}
* @return 日志id
*/
@Override
public String addLog(LogAddReq req) {
LogEntity logEntity = BeanUtil.copyProperties(req, LogEntity.class);
logEntity.setTimestamp(System.currentTimeMillis());
logMapper.save(logEntity);
return logEntity.getId();
}
/**
* 通过日志id查询日志详情
*
* @param id 日志id
* @return 日志
*/
@Override
public LogResp findLogById(String id) {
return logMapper.findById(id)
.map(logEntity -> BeanUtil.copyProperties(logEntity, LogResp.class))
.orElse(null);
}
/**
* 通过筛查条件查询日志列表
* <p>
* 若所有条件为空则返回按时间序倒排的1000条日志
* </p>
*
* @param req 查询条件 {@link LogFindReq}
* @return 按时间倒序排序的日志列表
*/
@Override
public List<LogResp> findLogsWithExample(LogFindReq req) {
if (Objects.isNull(req) || isAllFieldsNullOrBlank(req)) {
// 按照时间倒序即字段timestamp查询1000条
List<LogEntity> topLogs = findTopLimitOrderByTimestamp(1, 1000L);
if (ObjectUtil.isNotEmpty(topLogs)) {
return BeanUtil.copyToList(topLogs, LogResp.class);
}
return Collections.emptyList();
}
// 根据条件查询
Query query = buildQueryCriteria(req);
List<LogEntity> logs = mongoTemplate.find(query, LogEntity.class);
if (CollUtil.isNotEmpty(logs)) {
return BeanUtil.copyToList(logs, LogResp.class);
}
return Collections.emptyList();
}
/**
* 构建查询条件
*
* @param req 查询条件 {@link LogFindReq}
* @return Query对象
*/
@NotNull
private static Query buildQueryCriteria(LogFindReq req) {
Criteria criteria = new Criteria();
if (CharSequenceUtil.isNotBlank(req.getScene())) {
criteria.and("scene").regex(req.getScene());
}
if (CharSequenceUtil.isNotBlank(req.getLevel())) {
criteria.and("level").is(req.getLevel());
}
if (CollUtil.isNotEmpty(req.getTags())) {
criteria.and("tags").all(req.getTags());
}
if (CharSequenceUtil.isNotBlank(req.getMessage())) {
criteria.and("message").regex(req.getMessage());
}
if (NumberUtil.isPositiveNumber(req.getStart())) {
criteria.and("timestamp").gte(req.getStart());
}
if (NumberUtil.isPositiveNumber(req.getEnd())) {
criteria.and("timestamp").lte(req.getEnd());
}
Sort sort = Sort.by(Sort.Direction.DESC, "timestamp");
Query query = new Query(criteria);
query.with(sort);
return query;
}
/**
* 查询按timestamp字段指定顺序排序后的指定条数日志记录
*
* @param order 1:升序-1:降序
* @param size 查询条数
* @return 日志列表
*/
List<LogEntity> findTopLimitOrderByTimestamp(Integer order, Long size) {
Criteria criteria = Criteria.where("timestamp").exists(true);
Query query = new Query(criteria);
query.with(Sort.by(order > 0 ? Sort.Direction.ASC : Sort.Direction.DESC, "timestamp"))
.limit(size.intValue());
return mongoTemplate.find(query, LogEntity.class);
}
/**
* 判断对象的所有字段是否都为空或空字符串
*
* @param obj 需要检查的对象
* @return 如果所有字段都是 null ""返回 true否则返回 false
*/
public static boolean isAllFieldsNullOrBlank(Object obj) {
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
try {
// 设置访问权限因为有些字段可能是私有的
field.setAccessible(true);
Object value = field.get(obj);
if (value instanceof String) {
if (!((String) value).trim().isEmpty()) {
return false; // 字段不是空字符串
}
} else if (value != null) {
return false; // 字段不为 null
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Error accessing field", e);
}
}
return true;
}
}

View File

@ -4,7 +4,7 @@ spring:
cloud:
nacos:
config:
server-addr: ${NACOS_HOST:dev-nacos.axzo.cn}:${NACOS_PORT:80}
server-addr: ${NACOS_HOST:https://dev-nacos.axzo.cn}:${NACOS_PORT:443}
file-extension: yaml
namespace: ${NACOS_NAMESPACE_ID:35eada10-9574-4db8-9fea-bc6a4960b6c7}
prefix: ${spring.application.name}

View File

@ -0,0 +1,35 @@
type Query {
findLogsWithExample(logFindReq: LogFindReq): [LogResp]
findLogById(id: ID!): LogResp
}
type Mutation {
addLog(logReq: LogAddReq): String!
}
type LogResp {
id: ID!
scene: String!
level: String!
timestamp: String!
tags: [String]
message: String!
}
# 查询日志入参
input LogFindReq {
scene: String
level: String
tags: [String]
message: String
start: String
end: String
}
# 添加日志入参
input LogAddReq {
scene: String!
level: String!
tags: [String]
message: String!
}

View File

@ -0,0 +1,35 @@
package cn.axzo.log.platform.server.api;
import cn.axzo.log.platform.client.model.req.LogAddReq;
import cn.axzo.log.platform.server.LogPlatApplicationTestBase;
import cn.axzo.log.platform.server.controller.api.LogController;
import cn.axzo.log.platform.server.service.MongoLogOperationTest;
import cn.hutool.json.JSONUtil;
import com.google.common.collect.Lists;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 17:10
*/
public class LogApiTest extends LogPlatApplicationTestBase {
@Autowired
private LogController logApi;
@Test
public void addLog() {
MongoLogOperationTest.User user = new MongoLogOperationTest.User();
user.setName("test");
user.setAge(18);
LogAddReq logAddReq = LogAddReq.builder()
.scene("test")
.level("INFO")
.tags(Lists.newArrayList("tag1", "tag2", "tag3", "demo", "test"))
.message(JSONUtil.toJsonStr(user))
.build();
logApi.addLog(logAddReq);
}
}

View File

@ -0,0 +1,105 @@
package cn.axzo.log.platform.server.service;
import cn.axzo.log.platform.server.LogPlatApplicationTestBase;
import cn.axzo.log.platform.server.entity.LogEntity;
import cn.axzo.log.platform.server.mapper.LogMapper;
import cn.hutool.json.JSONUtil;
import com.google.common.collect.Lists;
import lombok.Data;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/10 16:37
*/
public class MongoLogOperationTest extends LogPlatApplicationTestBase {
@Autowired
private LogMapper logMapper;
@Test
public void insertOne() {
User user = new User();
user.setName("whymechen");
user.setAge(18);
LogEntity logEntity = LogEntity.builder()
.scene("add user")
.level("INFO")
.timestamp(System.currentTimeMillis())
.tags(Arrays.asList(new String[]{"user", "axzo-log-plat"}))
.message(JSONUtil.toJsonStr(user))
.build();
logMapper.insert(logEntity);
}
@Test
public void insertMany() {
User user = new User();
user.setName("lucy");
user.setAge(20);
LogEntity logEntity = LogEntity.builder()
.scene("add user")
.level("INFO")
.timestamp(System.currentTimeMillis())
.tags(Arrays.asList(new String[]{"user", "axzo-log-plat"}))
.message(JSONUtil.toJsonStr(user))
.build();
Animal animal = new Animal();
animal.setName("dog");
animal.setType("dog");
animal.setColor("black");
LogEntity logEntity2 = LogEntity.builder()
.scene("add animal")
.level("INFO")
.timestamp(System.currentTimeMillis())
.tags(Arrays.asList(new String[]{"animal", "axzo-log-plat"}))
.message(JSONUtil.toJsonStr(animal))
.build();
logMapper.insert(Lists.newArrayList(logEntity, logEntity2));
}
@Test
public void findAll() {
List<LogEntity> all = logMapper.findAll();
System.out.println(all);
// logMapper.findAllByTimestampBetween(LocalDateTime.now().minusDays(1), LocalDateTime.now());
}
@Test
public void findTagWithUser() {
LogEntity logEntity = new LogEntity();
logEntity.setTags(Arrays.asList(new String[]{"user"}));
Example<LogEntity> example = Example.of(logEntity);
List<LogEntity> all = logMapper.findAll(example);
System.out.println(all);
// logMapper.findAllByTimestampBetween(LocalDateTime.now().minusDays(1), LocalDateTime.now());
}
public
@Data
static class User {
private String name;
private Integer age;
}
@Data
static class Animal {
private String name;
private String type;
private String color;
}
}