feat: 完成分享、单会话与发布审批改造

- 增加工作流协作分享与知识库卡片分享入口,统一低版本浏览器复制反馈

- Web 新登录替换旧会话,并保持 API Key 会话隔离

- 发布审批增加必填说明并在审批详情展示

- 账号重置与导入改用可配置默认强密码
This commit is contained in:
2026-07-23 16:09:31 +08:00
parent caa1f07b66
commit 5a42826d44
71 changed files with 3191 additions and 132 deletions

View File

@@ -0,0 +1,123 @@
package tech.easyflow.admin.controller.ai;
import jakarta.servlet.http.HttpServletRequest;
import org.testng.Assert;
import org.testng.annotations.Test;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Locale;
import java.util.Map;
/**
* {@link WorkflowShareController} 分享地址构建测试。
*/
public class WorkflowShareControllerTest {
/**
* 验证分享地址保留前端部署基路径。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void shouldPreserveFrontendBasePathFromReferer() throws Exception {
HttpServletRequest request = request(Map.of(
"referer",
"https://example.test/easyflow/ai/workflow?page=1"
));
Assert.assertEquals(
buildShareBaseUrl(request),
"https://example.test/easyflow/ai/workflow/design"
);
}
/**
* 验证反向代理头用于构建外部 HTTPS 分享地址。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void shouldUseForwardedOriginAndPrefix() throws Exception {
HttpServletRequest request = request(Map.of(
"x-forwarded-proto", "https",
"x-forwarded-host", "example.test",
"x-forwarded-prefix", "/easyflow"
));
Assert.assertEquals(
buildShareBaseUrl(request),
"https://example.test/easyflow/ai/workflow/design"
);
}
/**
* 调用控制器的分享基础地址构建方法。
*
* @param request 模拟 HTTP 请求
* @return 分享基础地址
* @throws Exception 反射调用失败时抛出
*/
private String buildShareBaseUrl(HttpServletRequest request) throws Exception {
Method method = WorkflowShareController.class.getDeclaredMethod(
"buildShareBaseUrl",
HttpServletRequest.class
);
method.setAccessible(true);
return (String) method.invoke(new WorkflowShareController(), request);
}
/**
* 创建仅提供请求头能力的轻量 Servlet 请求代理。
*
* @param headers 小写请求头映射
* @return HTTP 请求代理
*/
private HttpServletRequest request(Map<String, String> headers) {
return (HttpServletRequest) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class<?>[]{HttpServletRequest.class},
(proxy, method, args) -> {
if ("getHeader".equals(method.getName())) {
String name = String.valueOf(args[0]).toLowerCase(Locale.ROOT);
return headers.get(name);
}
return defaultValue(method.getReturnType());
}
);
}
/**
* 返回代理方法所需的基础类型默认值。
*
* @param returnType 返回值类型
* @return 对应默认值
*/
private Object defaultValue(Class<?> returnType) {
if (!returnType.isPrimitive()) {
return null;
}
if (boolean.class == returnType) {
return false;
}
if (char.class == returnType) {
return '\0';
}
if (byte.class == returnType) {
return (byte) 0;
}
if (short.class == returnType) {
return (short) 0;
}
if (int.class == returnType) {
return 0;
}
if (long.class == returnType) {
return 0L;
}
if (float.class == returnType) {
return 0F;
}
return 0D;
}
}