feat: 提交流程中心后端代码
build / build-push (push) Successful in 28s

Spring Boot 流程引擎服务:流程定义/实例/任务、审批人解析、
回调分发、oapi 接口及单元测试。
保留远程 .gitea/workflows/build.yml 与 docker/Dockerfile,
新增 .gitignore 排除 target/ 构建产物。
This commit is contained in:
chenxing
2026-09-15 10:42:56 +08:00
parent 0296b2b5dd
commit c1103aeb29
65 changed files with 7119 additions and 0 deletions
@@ -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);
}
}