Spring Boot 流程引擎服务:流程定义/实例/任务、审批人解析、 回调分发、oapi 接口及单元测试。 保留远程 .gitea/workflows/build.yml 与 docker/Dockerfile, 新增 .gitignore 排除 target/ 构建产物。
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package com.yunhai.flowcenter.support;
|
||||
|
||||
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
|
||||
import com.yunhai.flowcenter.entity.FlwCallbackLog;
|
||||
import com.yunhai.flowcenter.entity.FlwCallbackReg;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwCallbackLogRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwCallbackRegRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwTaskRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* IT 公共基类:专用测试库 bsch_flowcenter_test,每例清表(引擎测试不能开测试事务——
|
||||
* AFTER_COMMIT 推进监听器依赖真实提交);提供定义落库/发布、映射注册的快捷方法。
|
||||
*/
|
||||
public abstract class FlowTestSupport {
|
||||
|
||||
public static final long SCH = 1L;
|
||||
|
||||
@Autowired
|
||||
protected FlwDefRepository defRepository;
|
||||
@Autowired
|
||||
protected FlwInstanceRepository instanceRepository;
|
||||
@Autowired
|
||||
protected FlwTaskRepository taskRepository;
|
||||
@Autowired
|
||||
protected FlwAssigneeMapRepository mapRepository;
|
||||
@Autowired
|
||||
protected FlwCallbackRegRepository regRepository;
|
||||
@Autowired
|
||||
protected FlwCallbackLogRepository logRepository;
|
||||
|
||||
protected void cleanDb() {
|
||||
logRepository.deleteAll();
|
||||
regRepository.deleteAll();
|
||||
taskRepository.deleteAll();
|
||||
instanceRepository.deleteAll();
|
||||
defRepository.deleteAll();
|
||||
mapRepository.deleteAll();
|
||||
}
|
||||
|
||||
protected FlwAssigneeMap mapDept(String deptKey, Integer userId, String userName) {
|
||||
FlwAssigneeMap map = new FlwAssigneeMap();
|
||||
map.setSchId(SCH);
|
||||
map.setDeptKey(deptKey);
|
||||
map.setDeptName(deptKey + "科");
|
||||
map.setUserId(userId);
|
||||
map.setUserName(userName);
|
||||
return mapRepository.save(map);
|
||||
}
|
||||
|
||||
protected FlwDef saveAndPublishDef(String code, String name, String contentJson) {
|
||||
FlwDef def = new FlwDef();
|
||||
def.setSchId(SCH);
|
||||
def.setFlowGuid(UUID.randomUUID().toString().replace("-", "").toLowerCase());
|
||||
def.setCode(code);
|
||||
def.setName(name);
|
||||
def.setVersion(1);
|
||||
def.setStatus(1);
|
||||
def.setPublishTime(java.time.LocalDateTime.now());
|
||||
def.setContentJson(contentJson);
|
||||
return defRepository.save(def);
|
||||
}
|
||||
|
||||
/** 线性流程:START → deptA 审批(ALL 会签)→ END */
|
||||
protected String linearDef(String deptKey, String nodeName) {
|
||||
return "{"
|
||||
+ "\"code\":\"" + codeOf() + "\",\"name\":\"线性测试流程\",\"start\":\"start\","
|
||||
+ "\"nodes\":["
|
||||
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"" + nodeName + "\","
|
||||
+ "\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptKey + "\"},\"countersign\":\"ALL\"},"
|
||||
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
||||
+ "],"
|
||||
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"end\"}],"
|
||||
+ "\"settings\":{\"rejectPolicy\":\"END_REJECTED\"}}";
|
||||
}
|
||||
|
||||
/** 并行流程:START → split → 两分支(deptA/deptB)→ join → END */
|
||||
protected String parallelDef(String deptA, String deptB) {
|
||||
return "{"
|
||||
+ "\"code\":\"" + codeOf() + "\",\"name\":\"并行测试流程\",\"start\":\"start\","
|
||||
+ "\"nodes\":["
|
||||
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
||||
+ "{\"id\":\"split1\",\"type\":\"PARALLEL_SPLIT\"},"
|
||||
+ "{\"id\":\"t_a\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}},"
|
||||
+ "{\"id\":\"t_b\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptB + "\"}},"
|
||||
+ "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
|
||||
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
||||
+ "],"
|
||||
+ "\"edges\":[{\"from\":\"start\",\"to\":\"split1\"},"
|
||||
+ "{\"from\":\"split1\",\"to\":\"t_a\"},{\"from\":\"split1\",\"to\":\"t_b\"},"
|
||||
+ "{\"from\":\"t_a\",\"to\":\"join1\"},{\"from\":\"t_b\",\"to\":\"join1\"},"
|
||||
+ "{\"from\":\"join1\",\"to\":\"end\"}],"
|
||||
+ "\"settings\":{\"rejectPolicy\":\"END_REJECTED\"}}";
|
||||
}
|
||||
|
||||
private static String codeOf() {
|
||||
return "tc_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.yunhai.flowcenter.support;
|
||||
|
||||
import com.yunhai.platform.utils.crypto.SignUtil;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* oapi 契约测试客户端:手工三件套签名(X-App-Key/X-Timestamp/X-Signature,
|
||||
* SignUtil.signRequest 请求级 canonical——与服务端 AppAuthFilter 验签同款)。
|
||||
* 故意不用共享 IDHttp:测试要能看到 401 原始响应。
|
||||
*/
|
||||
public class OapiSignedClient {
|
||||
|
||||
private final String baseUrl;
|
||||
private final String appKey;
|
||||
private final String appSecret;
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
public OapiSignedClient(String baseUrl, String appKey, String appSecret) {
|
||||
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
|
||||
this.appKey = appKey;
|
||||
this.appSecret = appSecret;
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(3000);
|
||||
factory.setReadTimeout(5000);
|
||||
this.restTemplate = new RestTemplate(factory);
|
||||
// 4xx/5xx 不抛异常,返回原始响应供断言(401/500 契约用例需要拿到状态码)
|
||||
this.restTemplate.setErrorHandler(new org.springframework.web.client.ResponseErrorHandler() {
|
||||
@Override
|
||||
public boolean hasError(org.springframework.http.client.ClientHttpResponse response) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(org.springframework.http.client.ClientHttpResponse response) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public <T> ResponseEntity<T> get(String pathAndQuery, Class<T> type) {
|
||||
return exchange(HttpMethod.GET, pathAndQuery, null, type);
|
||||
}
|
||||
|
||||
public <T> ResponseEntity<T> post(String pathAndQuery, Object body, Class<T> type) {
|
||||
return exchange(HttpMethod.POST, pathAndQuery, body, type);
|
||||
}
|
||||
|
||||
private <T> ResponseEntity<T> exchange(HttpMethod method, String pathAndQuery, Object body, Class<T> type) {
|
||||
String target = signTarget(pathAndQuery);
|
||||
String ts = String.valueOf(System.currentTimeMillis());
|
||||
String sign = SignUtil.signRequest(method.name(), target, ts, appSecret);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("X-App-Key", appKey);
|
||||
headers.set("X-Timestamp", ts);
|
||||
headers.set("X-Signature", sign);
|
||||
if (body != null) {
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
}
|
||||
return restTemplate.exchange(baseUrl + pathAndQuery, method,
|
||||
body == null ? new HttpEntity<>(headers) : new HttpEntity<>(body, headers), type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 与服务端对齐的签名 target:path + "?" + 排序后的 query(SignCanonical.sortRawQuery 语义,
|
||||
* 单参数场景天然有序;多参数时按 key 字典序拼接)。
|
||||
*/
|
||||
private static String signTarget(String pathAndQuery) {
|
||||
int q = pathAndQuery.indexOf('?');
|
||||
if (q < 0) {
|
||||
return pathAndQuery;
|
||||
}
|
||||
String path = pathAndQuery.substring(0, q);
|
||||
String[] pairs = pathAndQuery.substring(q + 1).split("&");
|
||||
java.util.Arrays.sort(pairs);
|
||||
StringBuilder sb = new StringBuilder(path);
|
||||
for (String pair : pairs) {
|
||||
if (sb.indexOf("?") < 0) {
|
||||
sb.append("?");
|
||||
} else if (sb.charAt(sb.length() - 1) != '?') {
|
||||
sb.append("&");
|
||||
}
|
||||
sb.append(pair);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** 无签名调用(401 用例) */
|
||||
public ResponseEntity<String> getRaw(String pathAndQuery) {
|
||||
return restTemplate.getForEntity(baseUrl + pathAndQuery, String.class);
|
||||
}
|
||||
|
||||
/** 带错误密钥的调用(401 用例) */
|
||||
public ResponseEntity<String> getWithBadSecret(String pathAndQuery) {
|
||||
String ts = String.valueOf(System.currentTimeMillis());
|
||||
String sign = SignUtil.signRequest("GET", signTarget(pathAndQuery), ts, "bad-secret-000000000000");
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("X-App-Key", appKey);
|
||||
headers.set("X-Timestamp", ts);
|
||||
headers.set("X-Signature", sign);
|
||||
return restTemplate.exchange(baseUrl + pathAndQuery, HttpMethod.GET,
|
||||
new HttpEntity<>(headers), String.class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user