Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
yimfeng authored Dec 23, 2021
2 parents 8d81722 + 71e1ffb commit ae11440
Show file tree
Hide file tree
Showing 69 changed files with 898 additions and 215 deletions.
16 changes: 8 additions & 8 deletions README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,23 @@
##### 如何使用
##### 环境依赖
- mac/linux/windows
- java 1.8
- mysql 服务端
- mac/linux/windows
- java 1.8
- mysql 服务端

##### 下载
```
git clone https://github.com/didi/AgileTC.git
git clone https://github.com/didi/AgileTC.git
或者 直接Download ZIP
```

##### 准备
- 创建依赖数据库,application-dev.properties中配置数据库名称为case_managercreate database case_manager
- 利用sql中的脚本配置对应表。创建脚本路径:case-server/sql/case-server.sql
- 修改application-dev.properties中spring.datasource的配置。默认数据库端口号为3306
- 创建依赖数据库,application-dev.properties中配置数据库名称为case_manager `create database case_manager`
- 利用sql中的脚本配置对应表。创建脚本路径:`case-server/sql/case-server.sql`
- 修改application-dev.properties中spring.datasource的配置。默认数据库端口号为3306

##### 运行
- mvn spring-boot:run (在case-server目录下执行)
- `mvn spring-boot:run` (在case-server目录下执行)
- 浏览器打开 http://localhost:8094/case/caseList/1

#### 整体架构
Expand Down
34 changes: 34 additions & 0 deletions case-server/edit_process.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
### 心跳
websocket底层基于TCP,是可靠连接,协议层有keepAlive机制来保障连接的可靠性。但是应用层依旧需要有心跳机制。

a) 是保障服务端可以正常响应客户端的请求。对于服务端cpu或内存过高,可能协议层依旧是连接状态,但是服务端实际无法响应客户端请求。

b) 存在一种场景,客户端断开连接后,服务端长时间无法感知,客户端也不知道自身依旧断连,此时如果有心跳机制,可以第一时间发现断连问题。

心跳从客户端还是服务端发起?

网上暂无定论。待讨论。早期的Agiletc是从服务端发起的,当前采用的双向ping的方案。

![image](https://dpubstatic.udache.com/static/dpubimg/9960809b-c0e0-4cb3-8b46-73c9c311c851.png)

### 编辑

![image](https://dpubstatic.udache.com/static/dpubimg/2735bfbd-972f-4b5a-a6cb-2b4913a7bb32.png)

### 保存
刷新或者退出页面,会触发保存。此时的保存场景整体分成2种,1.最后一个编辑者退出,触发的保存;2.非最后一个编辑者退出,触发的保存。 而保存会触发http的保存和ws onclose中的保存逻辑,根据时序的差异,又会有2种场景。2种保存场景都需要考虑时序问题。

- 最后一个编辑者退出,触发的保存

http保存会落库,并备份到backup表。

onclose中的保存会比较数据库中内容和待保存的case content,比较其中的版本号(base值),如果case content版本号更大,则保存,否则不保存。

- 非最后一个编辑者退出,触发的保存

http保存中,比较待保存内容与ws实例中的case content,如果没有差异,则将用例内容落库,并备份到backup表。如果有差异,则计算差异,并且以待保存内容为准,更新ws实例中的case content。并将差异发送给其他客户端。

onclose保存不做保存动作,仅移除player。

### websocket实例关系图
![image](https://dpubstatic.udache.com/static/dpubimg/ea03cf9a-1b85-4276-b4f4-01e959e688e3.png)
35 changes: 32 additions & 3 deletions case-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<druid.version>1.1.16</druid.version>
<json.version>1.2.70</json.version>
<common.version>3.4</common.version>
<websocket.version>5.1.9.RELEASE</websocket.version>
<websocket.version>5.1.18.RELEASE</websocket.version>
<page.helper.version>1.2.5</page.helper.version>
</properties>

Expand Down Expand Up @@ -52,6 +52,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.3.8</version>
</dependency>

<!-- springboot所依赖的mybatis-boot包 -->
<dependency>
Expand Down Expand Up @@ -126,6 +131,26 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>com.lmax</groupId>
Expand All @@ -142,9 +167,13 @@
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.1</version>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>

<!-- HTTP相关的组件 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ public class SystemConstant {

public static final String HTTP_USER_AGENT = "User-Agent";

public static final String DEFAULT_AUTHORITY_NAME = "ROLE_USER";

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public enum StatusCode implements Status {
FILE_FORMAT_ERROR(10007, "文件格式不对,请上传xmind文件"),
FILE_IMPORT_ERROR(10008, "导入失败,请稍后再试"),
FILE_EXPORT_ERROR(10009, "导出失败,请稍后再试"),
EXCEL_FORMAT_ERROR(10010, "EXCEL格式不匹配"),
NODE_ALREADY_EXISTS(20001, "节点已存在"),
WS_UNKNOWN_ERROR(100010, "websocket访问异常"),
AUTHORITY_ERROR(100011, "权限认证错误"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.xiaoju.framework.entity.exception.CaseServerException;
import com.xiaoju.framework.entity.request.dir.DirCreateReq;
import com.xiaoju.framework.entity.request.dir.DirDeleteReq;
import com.xiaoju.framework.entity.request.dir.DirMoveReq;
import com.xiaoju.framework.entity.request.dir.DirRenameReq;
import com.xiaoju.framework.entity.response.controller.Response;
import com.xiaoju.framework.service.DirService;
Expand Down Expand Up @@ -120,5 +121,27 @@ public Response<?> getDirCardTree(@RequestParam @NotNull(message = "业务线id
@GetMapping("/getId")
public String getId(String parentId, Long productLineId, Integer channel, String text) {
return dirService.getId(parentId, productLineId, channel, text);

/**
* 移动文件夹,会文件夹下及其子文件夹全部移动
* 不允许移动root文件夹 -- 通过传参校验
* 不允许从父文件夹移动到子文件夹 -- 通过dfs路径计算
* 不允许移动不存在的文件夹,或者移动到不存在的文件夹 -- 通过dfs埋点
*
* @param req 请求体
* @return 响应体
*/
@PostMapping(value = "/move")
public Response<?> moveDir(@RequestBody DirMoveReq req) {
req.validate();
try {
return Response.success(dirService.moveDir(req));
} catch (CaseServerException e) {
throw new CaseServerException(e.getLocalizedMessage(), e.getStatus());
} catch (Exception e) {
LOGGER.error("[Dir move]move dir failed. params={} e={} ", req.toString(), e.getMessage());
e.printStackTrace();
return Response.build(StatusCode.SERVER_BUSY_ERROR);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.xiaoju.framework.controller;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.xiaoju.framework.constants.SystemConstant;
Expand All @@ -10,27 +9,20 @@
import com.xiaoju.framework.entity.response.cases.ExportXmindResp;
import com.xiaoju.framework.entity.response.controller.Response;
import com.xiaoju.framework.service.FileService;
import com.xiaoju.framework.util.FileUtil;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.UUID;

/**
* 文件上传与导出
Expand Down Expand Up @@ -80,36 +72,40 @@ public Response<Long> importXmind(@RequestParam MultipartFile file, String creat
}
}


@PostMapping(value = "/importExcel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Response<Long> importExcel(@RequestParam MultipartFile file, String creator, String bizId,
Long productLineId, String title, String description, Integer channel, String requirementId, HttpServletRequest request) {
FileImportReq req = new FileImportReq(file, creator, productLineId, title, description, channel, requirementId, bizId);
req.validate();
try {
return Response.success(fileService.importExcelFile(req, request));
} catch (CaseServerException e) {
throw new CaseServerException(e.getLocalizedMessage(), e.getStatus());
} catch (Exception e) {
LOGGER.error("[导入x-excel出错] 传参req={},错误原因={}", req.toString(), e.getMessage());
e.printStackTrace();
return Response.build(StatusCode.FILE_IMPORT_ERROR.getStatus(), StatusCode.FILE_IMPORT_ERROR.getMsg());
}
}

@PostMapping(value = "/uploadAttachment", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public JSONObject uploadAttachment(@RequestParam MultipartFile file, HttpServletRequest request) {

JSONObject ret = new JSONObject();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd/");
String format = sdf.format(new Date());
File folder = new File(uploadPath + format);// 文件夹的名字
if (!folder.isDirectory()) { // 如果文件夹为空,则新建文件夹
folder.mkdirs();
}
// 对上传的文件重命名,避免文件重名
String oldName = file.getOriginalFilename(); // 获取文件的名字
String newName = UUID.randomUUID().toString()
+ oldName.substring(oldName.lastIndexOf("."), oldName.length()); // 生成新的随机的文件名字
try {
file.transferTo(new File(folder, newName));
String fileUrlPath = FileUtil.fileUpload(uploadPath, file);

// 返回上传文件的访问路径
// request.getScheme()可获取请求的协议名,request.getServerName()可获取请求的域名,request.getServerPort()可获取请求的端口号
String filePath = request.getScheme() + "://" + request.getServerName()
+ ":" + request.getServerPort() + "/" + format + newName;
+ ":" + request.getServerPort() + "/" + fileUrlPath;
JSONArray datas = new JSONArray();
JSONObject data = new JSONObject();
data.put("url", filePath);
ret.put("success", 1);
datas.add(data);
ret.put("data", datas);
return ret;
} catch (IOException e) {
} catch (Exception e) {
LOGGER.error("上传文件失败, 请重试。", e);
ret.put("success", 0);
ret.put("data", "");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.xiaoju.framework.entity.request.dir;

import com.xiaoju.framework.constants.BizConstant;
import com.xiaoju.framework.entity.request.ParamValidate;
import lombok.Data;
import org.springframework.util.StringUtils;

/**
* 目录下用例全部迁移的请求体
* Created by didi on 2021/12/14.
*/
@Data
public class DirMoveReq implements ParamValidate {
private Integer channel;

private Long productLineId;

/**
* 被选中文件夹的id
*/
private String fromId;

/**
* 如果想移到同级,设置为选中文件夹的parentId
* 如果想要移到自己,设为选中文件夹的Id
*/
private String toId;

@Override
public void validate() {
if (productLineId == null || productLineId <= 0) {
throw new IllegalArgumentException("业务线id为空或者非法");
}
if (StringUtils.isEmpty(fromId) || StringUtils.isEmpty(toId)) {
throw new IllegalArgumentException("来源或迁移文件夹id不能为空");
}
if (BizConstant.ROOT_BIZ_ID.equals(fromId)) {
throw new IllegalArgumentException("根文件夹暂不支持迁移");
}
if (fromId.equals(toId)) {
throw new IllegalArgumentException("相同的文件夹不需要迁移");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
import com.alibaba.fastjson.JSONObject;
import com.xiaoju.framework.constants.enums.StatusCode;
import com.xiaoju.framework.entity.dto.Authority;
import com.xiaoju.framework.mapper.AuthorityMapper;
import com.xiaoju.framework.mapper.UserMapper;
import com.xiaoju.framework.service.UserService;
import com.xiaoju.framework.util.CookieUtils;
import org.apache.tomcat.websocket.server.WsFilter;
import org.slf4j.Logger;
Expand All @@ -29,27 +28,15 @@
@Component
public class WebSocketFilter extends FilterRegistrationBean<WsFilter> {

// 保存对应权限的路径匹配列表
private Map<String, List<String>> roleAuthority = new HashMap<>();

@Resource
private AuthorityMapper authorityMapper;
@Resource
private UserMapper userMapper;
private UserService userService;

@Value("${authority.flag}")
private Boolean authorityFlag;

@PostConstruct
public void init() {

// authority表中id1,2,3分别表示普通用户、管理员、超管
for (long i = 1; i < 4; i++) {
Authority authority = authorityMapper.selectByPrimaryKey(i);
String[] authorityContent = authority.getAuthorityContent().split(",");

roleAuthority.put(authority.getAuthorityName(), Arrays.asList(authorityContent));
}
setFilter(new WsAuthFilter());
setUrlPatterns(Arrays.asList("/api/dir/*", "/api/backup/*", "/api/record/*", "/api/file/*", "/api/case/*"));
}
Expand Down Expand Up @@ -77,9 +64,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
private void authenticateByCookie(HttpServletRequest req) {
if (!authorityFlag) return;
String username = CookieUtils.getCookieValue(req, "username");
String authorityName = userMapper.selectByUserName(username).getAuthorityName();

List<String> authorityContent = roleAuthority.get(StringUtils.isEmpty(authorityName)?"ROLE_USER":authorityName);
// 获取用户对应权限的路径匹配列表
List<String> authorityContent = userService.getUserAuthorityContent(username);

String pathInfo = req.getPathInfo();
String path;
Expand Down
Loading

0 comments on commit ae11440

Please sign in to comment.