From ddee4da72a209a87332295e83fd7cd466ffcc9af Mon Sep 17 00:00:00 2001 From: chenbowen Date: Fri, 31 Oct 2025 09:28:59 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E6=96=B0=E5=A2=9E=20api=20=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95=EF=BC=8C=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E7=89=88=E6=9C=AC=E5=9B=9E=E6=BB=9A=202.=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E7=94=A8=E6=88=B7=E8=A7=92=E8=89=B2=E6=9D=83=E9=99=90?= =?UTF-8?q?=E7=9B=91=E7=9D=A3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 7 +- ...线API版本历史菜单权限_备忘录模式_20251030.sql | 33 ++ ...线API版本历史表结构_备忘录模式_20251030.sql | 52 +++ .../数据总线API访问日志菜单权限_20251028.sql | 13 + sql/dm/数据总线API访问日志表结构_20251028.sql | 0 sql/dm/权限监督功能.sql | 52 +++ .../pom.xml | 5 + .../core/util/BusinessDeptHandleUtil.java | 70 +++- .../web/CompanyVisitContextInterceptor.java | 78 ++++- .../admin/gateway/ApiAccessLogController.java | 52 +++ .../admin/gateway/ApiVersionController.java | 97 ++++++ .../gateway/convert/ApiAccessLogConvert.java | 29 ++ .../gateway/convert/ApiDefinitionConvert.java | 33 +- .../gateway/convert/ApiVersionConvert.java | 30 ++ .../vo/accesslog/ApiAccessLogPageReqVO.java | 53 +++ .../vo/accesslog/ApiAccessLogRespVO.java | 89 ++++++ .../vo/version/ApiVersionCompareRespVO.java | 72 +++++ .../vo/version/ApiVersionCreateReqVO.java | 26 ++ .../vo/version/ApiVersionDetailRespVO.java | 47 +++ .../vo/version/ApiVersionPageReqVO.java | 33 ++ .../gateway/vo/version/ApiVersionRespVO.java | 42 +++ .../vo/version/ApiVersionRollbackReqVO.java | 21 ++ .../dataobject/gateway/ApiAccessLogDO.java | 140 ++++++++ .../dal/dataobject/gateway/ApiVersionDO.java | 59 ++++ .../dal/mapper/gateway/ApiVersionMapper.java | 78 +++++ .../dal/mysql/gateway/ApiAccessLogMapper.java | 31 ++ .../gateway/core/ApiGatewayAccessLogger.java | 255 +++++++++++++++ .../core/ApiGatewayExecutionService.java | 22 +- .../gateway/core/ApiGatewayRequestMapper.java | 25 ++ .../gateway/model/ApiInvocationContext.java | 6 + .../service/gateway/ApiAccessLogService.java | 42 +++ .../service/gateway/ApiVersionService.java | 65 ++++ .../ApiVersionSnapshotContextHolder.java | 33 ++ .../gateway/impl/ApiAccessLogServiceImpl.java | 47 +++ .../impl/ApiDefinitionServiceImpl.java | 61 ++-- .../gateway/impl/ApiVersionServiceImpl.java | 299 +++++++++++++++++ .../GatewayServiceErrorCodeConstants.java | 6 + .../sample/DatabusApiInvocationExample.java | 11 +- .../permission/PermissionController.java | 301 +++++++++++++++++- .../PermissionUserSupervisionRespVO.java | 41 +++ .../admin/user/vo/user/UserPageReqVO.java | 4 + .../service/permission/MenuServiceImpl.java | 47 ++- .../service/user/AdminUserServiceImpl.java | 12 + 43 files changed, 2454 insertions(+), 65 deletions(-) create mode 100644 sql/dm/数据总线API版本历史菜单权限_备忘录模式_20251030.sql create mode 100644 sql/dm/数据总线API版本历史表结构_备忘录模式_20251030.sql create mode 100644 sql/dm/数据总线API访问日志菜单权限_20251028.sql create mode 100644 sql/dm/数据总线API访问日志表结构_20251028.sql create mode 100644 sql/dm/权限监督功能.sql create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiAccessLogController.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiVersionController.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiAccessLogConvert.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiVersionConvert.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogPageReqVO.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogRespVO.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionCompareRespVO.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionCreateReqVO.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionDetailRespVO.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionPageReqVO.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionRespVO.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionRollbackReqVO.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiAccessLogDO.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiVersionDO.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mapper/gateway/ApiVersionMapper.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiAccessLogMapper.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiAccessLogService.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiVersionService.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiVersionSnapshotContextHolder.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiAccessLogServiceImpl.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiVersionServiceImpl.java create mode 100644 zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/permission/PermissionUserSupervisionRespVO.java diff --git a/pom.xml b/pom.xml index 0cd0f7ce..d0860b78 100644 --- a/pom.xml +++ b/pom.xml @@ -205,8 +205,13 @@ 中铜 ZStack 私服 http://172.16.46.63:30708/repository/test/ - true + always + warn + + true + always + diff --git a/sql/dm/数据总线API版本历史菜单权限_备忘录模式_20251030.sql b/sql/dm/数据总线API版本历史菜单权限_备忘录模式_20251030.sql new file mode 100644 index 00000000..6c19e2d2 --- /dev/null +++ b/sql/dm/数据总线API版本历史菜单权限_备忘录模式_20251030.sql @@ -0,0 +1,33 @@ +-- ============================================= +-- 数据总线 API 版本历史菜单权限(备忘录模式) +-- 功能说明: +-- 1. 查看版本历史 +-- 2. 查看版本详情 +-- 3. 版本回滚 +-- 4. 版本对比 +-- ============================================= + +-- 删除旧的版本管理菜单权限 +DELETE FROM system_menu WHERE id IN (650107, 650108, 650109, 650110); + +-- 插入新的版本历史管理权限 +INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, + status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) +VALUES + -- 查询版本历史列表 + (650107, 'API版本历史', 'databus:gateway:version:query', 3, 7, 6501, '', '', '', '', + 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + -- 查看版本详情 + (650108, 'API版本详情', 'databus:gateway:version:detail', 3, 8, 6501, '', '', '', '', + 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + -- 版本回滚 + (650109, 'API版本回滚', 'databus:gateway:version:rollback', 3, 9, 6501, '', '', '', '', + 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + -- 版本对比 + (650110, 'API版本对比', 'databus:gateway:version:compare', 3, 10, 6501, '', '', '', '', + 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'); + +-- 说明 +-- 1. 不再需要"创建版本"权限,因为系统自动创建 +-- 2. 不再需要"删除版本"权限,版本历史不可删除 +-- 3. 保留查询、详情、回滚、对比四个核心功能 diff --git a/sql/dm/数据总线API版本历史表结构_备忘录模式_20251030.sql b/sql/dm/数据总线API版本历史表结构_备忘录模式_20251030.sql new file mode 100644 index 00000000..f2d2d819 --- /dev/null +++ b/sql/dm/数据总线API版本历史表结构_备忘录模式_20251030.sql @@ -0,0 +1,52 @@ +-- ============================================= +-- 数据总线 API 版本历史表(备忘录模式) +-- 功能说明: +-- 1. 每次保存 API 配置时自动创建版本记录 +-- 2. 版本号自动递增(v1, v2, v3...) +-- 3. 保留完整历史链,不可删除 +-- 4. 支持一键回滚到任意历史版本 +-- 5. 支持版本对比功能 +-- ============================================= + +-- 如果表已存在则删除 +DROP TABLE IF EXISTS databus_api_version; + +-- 创建版本历史表(DM8 语法) +CREATE TABLE databus_api_version ( + id BIGINT NOT NULL, + api_id BIGINT NOT NULL, + version_number INTEGER NOT NULL, + snapshot_data CLOB NOT NULL, + description VARCHAR(500), + is_current NUMBER(1) DEFAULT 0 NOT NULL, + operator VARCHAR(64), + creator VARCHAR(64) DEFAULT '' NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updater VARCHAR(64) DEFAULT '' NOT NULL, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + deleted NUMBER(1) DEFAULT 0 NOT NULL, + tenant_id BIGINT DEFAULT 0 NOT NULL, + CONSTRAINT pk_databus_api_version PRIMARY KEY (id) +); + +-- 创建索引 +CREATE INDEX idx_databus_api_version_api_id ON databus_api_version (api_id); +CREATE INDEX idx_databus_api_version_version_number ON databus_api_version (api_id, version_number); +CREATE INDEX idx_databus_api_version_is_current ON databus_api_version (api_id, is_current); +CREATE INDEX idx_databus_api_version_create_time ON databus_api_version (create_time); +CREATE INDEX idx_databus_api_version_operator ON databus_api_version (operator); + +COMMENT ON TABLE databus_api_version IS '数据总线API版本历史表:采用备忘录模式,每次保存API时自动创建版本快照,支持完整的版本历史追溯和回滚'; +COMMENT ON COLUMN databus_api_version.id IS '主键ID'; +COMMENT ON COLUMN databus_api_version.api_id IS 'API定义ID,关联databus_api_definition表'; +COMMENT ON COLUMN databus_api_version.version_number IS '版本号,同一API下自动递增(1,2,3...)'; +COMMENT ON COLUMN databus_api_version.snapshot_data IS 'API完整配置快照(JSON格式),包含definition、steps、transforms等所有信息'; +COMMENT ON COLUMN databus_api_version.description IS '变更说明,记录本次修改的内容'; +COMMENT ON COLUMN databus_api_version.is_current IS '是否为当前版本(1=是,0=否),同一API只有一个当前版本'; +COMMENT ON COLUMN databus_api_version.operator IS '操作人,记录谁创建了这个版本'; +COMMENT ON COLUMN databus_api_version.creator IS '创建者'; +COMMENT ON COLUMN databus_api_version.create_time IS '创建时间(版本创建时间)'; +COMMENT ON COLUMN databus_api_version.updater IS '更新者'; +COMMENT ON COLUMN databus_api_version.update_time IS '更新时间'; +COMMENT ON COLUMN databus_api_version.deleted IS '是否删除(逻辑删除,实际不删除版本历史)'; +COMMENT ON COLUMN databus_api_version.tenant_id IS '租户ID'; diff --git a/sql/dm/数据总线API访问日志菜单权限_20251028.sql b/sql/dm/数据总线API访问日志菜单权限_20251028.sql new file mode 100644 index 00000000..3a4369d4 --- /dev/null +++ b/sql/dm/数据总线API访问日志菜单权限_20251028.sql @@ -0,0 +1,13 @@ +-- 数据总线 API 访问日志菜单权限初始化(DM8) +-- 创建访问日志页面及查询按钮权限。如已存在将先行移除再新增。 +DELETE FROM system_menu WHERE id IN (6504, 650401); + +INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, + status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) +VALUES (6504, '访问日志', 'databus:gateway:access-log:query', 2, 40, 6500, 'access-log', 'ep:document', 'databus/accesslog/index', 'DatabusAccessLog', + 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'); + +INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, + status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) +VALUES (650401, '访问日志查询', 'databus:gateway:access-log:query', 3, 1, 6504, '', '', '', '', + 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'); diff --git a/sql/dm/数据总线API访问日志表结构_20251028.sql b/sql/dm/数据总线API访问日志表结构_20251028.sql new file mode 100644 index 00000000..e69de29b diff --git a/sql/dm/权限监督功能.sql b/sql/dm/权限监督功能.sql new file mode 100644 index 00000000..cb3e3676 --- /dev/null +++ b/sql/dm/权限监督功能.sql @@ -0,0 +1,52 @@ +-- 权限监督按钮及接口权限(DM8 专用) +-- 执行前请确认未占用 1068 主键 + +DELETE FROM system_menu WHERE id = 1068; + +INSERT INTO system_menu ( + id, + name, + permission, + type, + sort, + parent_id, + path, + icon, + component, + component_name, + status, + visible, + keep_alive, + always_show, + creator, + create_time, + updater, + update_time, + deleted +) +SELECT + 1068, + '权限监督', + 'system:permission:user-permission-supervision', + 3, + 9, + 101, + '', + '', + '', + NULL, + 0, + '1', + '1', + '1', + 'admin', + '2025-10-29 00:00:00', + '', + '2025-10-29 00:00:00', + '0' +FROM dual +WHERE NOT EXISTS ( + SELECT 1 + FROM system_menu + WHERE id = 1068 +); diff --git a/zt-framework/zt-spring-boot-starter-biz-business/pom.xml b/zt-framework/zt-spring-boot-starter-biz-business/pom.xml index 991aa53f..86e2487d 100644 --- a/zt-framework/zt-spring-boot-starter-biz-business/pom.xml +++ b/zt-framework/zt-spring-boot-starter-biz-business/pom.xml @@ -31,6 +31,11 @@ zt-spring-boot-starter-biz-data-permission ${revision} + + com.zt.plat + zt-spring-boot-starter-biz-tenant + ${revision} + com.zt.plat diff --git a/zt-framework/zt-spring-boot-starter-biz-business/src/main/java/com/zt/plat/framework/business/core/util/BusinessDeptHandleUtil.java b/zt-framework/zt-spring-boot-starter-biz-business/src/main/java/com/zt/plat/framework/business/core/util/BusinessDeptHandleUtil.java index 1dfa383a..4036ef85 100644 --- a/zt-framework/zt-spring-boot-starter-biz-business/src/main/java/com/zt/plat/framework/business/core/util/BusinessDeptHandleUtil.java +++ b/zt-framework/zt-spring-boot-starter-biz-business/src/main/java/com/zt/plat/framework/business/core/util/BusinessDeptHandleUtil.java @@ -4,11 +4,15 @@ import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.zt.plat.framework.common.pojo.CompanyDeptInfo; import com.zt.plat.framework.security.core.LoginUser; +import com.zt.plat.framework.tenant.core.context.CompanyContextHolder; +import com.zt.plat.framework.web.core.util.WebFrameworkUtils; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -22,43 +26,85 @@ import static com.zt.plat.framework.security.core.util.SecurityFrameworkUtils.ge public class BusinessDeptHandleUtil { public static Set getBelongCompanyAndDept(HttpServletRequest request, HttpServletResponse response) throws Exception { response.setContentType("application/json;charset=UTF-8"); - String companyId = request.getHeader("visit-company-id"); - String deptId = request.getHeader("visit-dept-id"); - LoginUser loginUser = Optional.ofNullable(getLoginUser()).orElse(new LoginUser().setInfo(new HashMap<>())); - Set companyDeptSet = JSONUtil.parseArray(loginUser.getInfo().getOrDefault(LoginUser.INFO_KEY_COMPANY_DEPT_SET, "[]")).stream() + String companyIdHeader = request.getHeader(WebFrameworkUtils.HEADER_VISIT_COMPANY_ID); + String deptIdHeader = request.getHeader(WebFrameworkUtils.HEADER_VISIT_DEPT_ID); + + LoginUser currentLoginUser = getLoginUser(); + Map extraInfo = Optional.ofNullable(currentLoginUser) + .map(LoginUser::getInfo) + .orElseGet(HashMap::new); + if (currentLoginUser != null && currentLoginUser.getInfo() == null) { + currentLoginUser.setInfo(extraInfo); + } + + Set companyDeptSet = JSONUtil.parseArray(extraInfo.getOrDefault(LoginUser.INFO_KEY_COMPANY_DEPT_SET, "[]")).stream() .map(obj -> JSONUtil.toBean((JSONObject) obj, CompanyDeptInfo.class)) .collect(Collectors.toSet()); // 1. 有 companyId - if (companyId != null && !companyId.isBlank()) { + if (companyIdHeader != null && !companyIdHeader.isBlank()) { // 根据请求头中的公司 ID 过滤出当前用户的公司部门信息 - Set companyDeptSetByCompanyId = companyDeptSet.stream().filter(companyDeptInfo -> companyDeptInfo.getCompanyId().toString().equals(companyId)).collect(Collectors.toSet()); + Set companyDeptSetByCompanyId = companyDeptSet.stream() + .filter(companyDeptInfo -> companyDeptInfo.getCompanyId().toString().equals(companyIdHeader)) + .collect(Collectors.toSet()); if (companyDeptSetByCompanyId.isEmpty()) { // 当前公司下没有部门 CompanyDeptInfo data = new CompanyDeptInfo(); - data.setCompanyId(Long.valueOf(companyId)); + data.setCompanyId(Long.valueOf(companyIdHeader)); data.setDeptId(0L); return new HashSet<>(singleton(data)); } // 如果有 deptId,校验其是否属于该 companyId - if (deptId != null) { - boolean valid = companyDeptSetByCompanyId.stream().anyMatch(info -> String.valueOf(info.getDeptId()).equals(deptId)); + if (deptIdHeader != null) { + boolean valid = companyDeptSetByCompanyId.stream().anyMatch(info -> String.valueOf(info.getDeptId()).equals(deptIdHeader)); if (!valid) { return null; - }else{ + } else { // 部门存在,放行 return new HashSet<>(); } } + if (companyDeptSetByCompanyId.size() == 1) { + CompanyDeptInfo singleCompanyDept = companyDeptSetByCompanyId.iterator().next(); + if (applyAutoSelection(currentLoginUser, request, singleCompanyDept)) { + return Collections.emptySet(); + } + } return companyDeptSetByCompanyId; } // 2. 没有公司信息,尝试唯一性自动推断 // 如果当前用户下只有一个公司和部门的对于关系 if (companyDeptSet.size() == 1) { CompanyDeptInfo companyDeptInfo = companyDeptSet.iterator().next(); + if (applyAutoSelection(currentLoginUser, request, companyDeptInfo)) { + return Collections.emptySet(); + } return new HashSet<>(singleton(companyDeptInfo)); - } else { - return companyDeptSet; } + return companyDeptSet; + } + + private static boolean applyAutoSelection(LoginUser loginUser, HttpServletRequest request, CompanyDeptInfo info) { + if (info == null || info.getCompanyId() == null || info.getCompanyId() <= 0 + || info.getDeptId() == null || info.getDeptId() <= 0) { + return false; + } + if (loginUser != null) { + loginUser.setVisitCompanyId(info.getCompanyId()); + loginUser.setVisitCompanyName(info.getCompanyName()); + loginUser.setVisitDeptId(info.getDeptId()); + loginUser.setVisitDeptName(info.getDeptName()); + } + request.setAttribute(WebFrameworkUtils.HEADER_VISIT_COMPANY_ID, info.getCompanyId()); + if (info.getCompanyName() != null) { + request.setAttribute(WebFrameworkUtils.HEADER_VISIT_COMPANY_NAME, info.getCompanyName()); + } + request.setAttribute(WebFrameworkUtils.HEADER_VISIT_DEPT_ID, info.getDeptId()); + if (info.getDeptName() != null) { + request.setAttribute(WebFrameworkUtils.HEADER_VISIT_DEPT_NAME, info.getDeptName()); + } + CompanyContextHolder.setIgnore(false); + CompanyContextHolder.setCompanyId(info.getCompanyId()); + return true; } } diff --git a/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptor.java b/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptor.java index 076f80c4..538f6d4f 100644 --- a/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptor.java +++ b/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptor.java @@ -20,30 +20,68 @@ public class CompanyVisitContextInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - // 解析 header 并设置 visitCompanyId + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + Long companyId = WebFrameworkUtils.getCompanyId(request); + // 优先使用请求头上的公司信息,若缺失则回退到请求属性或当前登录用户已缓存的访问公司 + if (companyId == null || companyId <= 0L) { + Long attrCompanyId = resolveLong(request.getAttribute(WebFrameworkUtils.HEADER_VISIT_COMPANY_ID)); + if (attrCompanyId != null && attrCompanyId > 0L) { + companyId = attrCompanyId; + } else if (loginUser != null && loginUser.getVisitCompanyId() != null && loginUser.getVisitCompanyId() > 0L) { + companyId = loginUser.getVisitCompanyId(); + } + } + String companyName = WebFrameworkUtils.getCompanyName(request); - if (companyId <= 0L) { - // 如果没有设置 companyId,则忽略 + if (companyName == null || companyName.isEmpty()) { + Object attrCompanyName = request.getAttribute(WebFrameworkUtils.HEADER_VISIT_COMPANY_NAME); + if (attrCompanyName instanceof String) { + companyName = (String) attrCompanyName; + } else if (loginUser != null) { + companyName = loginUser.getVisitCompanyName(); + } + } + + Long deptId = WebFrameworkUtils.getDeptId(request); + // 部门信息同样遵循“请求头 -> 请求属性 -> 登录缓存”的回退顺序 + if (deptId == null || deptId <= 0L) { + Long attrDeptId = resolveLong(request.getAttribute(WebFrameworkUtils.HEADER_VISIT_DEPT_ID)); + if (attrDeptId != null && attrDeptId > 0L) { + deptId = attrDeptId; + } else if (loginUser != null && loginUser.getVisitDeptId() != null && loginUser.getVisitDeptId() > 0L) { + deptId = loginUser.getVisitDeptId(); + } + } + + String deptName = WebFrameworkUtils.getDeptName(request); + if (deptName == null || deptName.isEmpty()) { + Object attrDeptName = request.getAttribute(WebFrameworkUtils.HEADER_VISIT_DEPT_NAME); + if (attrDeptName instanceof String) { + deptName = (String) attrDeptName; + } else if (loginUser != null) { + deptName = loginUser.getVisitDeptName(); + } + } + + if (companyId == null || companyId <= 0L) { CompanyContextHolder.setIgnore(true); return true; } - Long deptId = WebFrameworkUtils.getDeptId(request); - String deptName = WebFrameworkUtils.getDeptName(request); - LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + + CompanyContextHolder.setIgnore(false); + CompanyContextHolder.setCompanyId(companyId); if (loginUser == null) { return true; } - if (deptId > 0L) { + + // 同步最新的访问公司/部门到登录用户对象,供后续数据权限及上下文读取 + loginUser.setVisitCompanyId(companyId); + loginUser.setVisitCompanyName(companyName); + if (deptId != null && deptId > 0L) { loginUser.setVisitDeptId(deptId); loginUser.setVisitDeptName(deptName); } -// if (!securityFrameworkService.hasAnyPermissions(PERMISSION)) { -// throw exception0(GlobalErrorCodeConstants.FORBIDDEN.getCode(), "您无权切换部门"); -// } - loginUser.setVisitCompanyId(companyId); - loginUser.setVisitCompanyName(companyName); - CompanyContextHolder.setCompanyId(companyId); return true; } @@ -55,4 +93,18 @@ public class CompanyVisitContextInterceptor implements HandlerInterceptor { loginUser.setVisitCompanyId(0L); } } + + private Long resolveLong(Object value) { + if (value instanceof Number) { + return ((Number) value).longValue(); + } + if (value instanceof String) { + try { + return Long.parseLong(((String) value).trim()); + } catch (NumberFormatException ignored) { + return null; + } + } + return null; + } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiAccessLogController.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiAccessLogController.java new file mode 100644 index 00000000..6f7d93fa --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiAccessLogController.java @@ -0,0 +1,52 @@ +package com.zt.plat.module.databus.controller.admin.gateway; + +import com.zt.plat.framework.common.pojo.CommonResult; +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.module.databus.controller.admin.gateway.convert.ApiAccessLogConvert; +import com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog.ApiAccessLogPageReqVO; +import com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog.ApiAccessLogRespVO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO; +import com.zt.plat.module.databus.service.gateway.ApiAccessLogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static com.zt.plat.framework.common.pojo.CommonResult.success; + +/** + * Databus API 访问日志控制器。 + */ +@Tag(name = "管理后台 - Databus API 访问日志") +@RestController +@RequestMapping("/databus/gateway/access-log") +@Validated +public class ApiAccessLogController { + + @Resource + private ApiAccessLogService apiAccessLogService; + + @GetMapping("/get") + @Operation(summary = "获取访问日志详情") + @Parameter(name = "id", description = "日志编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('databus:gateway:access-log:query')") + public CommonResult get(@RequestParam("id") Long id) { + ApiAccessLogDO logDO = apiAccessLogService.get(id); + return success(ApiAccessLogConvert.INSTANCE.convert(logDO)); + } + + @GetMapping("/page") + @Operation(summary = "分页查询访问日志") + @PreAuthorize("@ss.hasPermission('databus:gateway:access-log:query')") + public CommonResult> page(@Valid ApiAccessLogPageReqVO pageReqVO) { + PageResult pageResult = apiAccessLogService.getPage(pageReqVO); + return success(ApiAccessLogConvert.INSTANCE.convertPage(pageResult)); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiVersionController.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiVersionController.java new file mode 100644 index 00000000..8c5c6845 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiVersionController.java @@ -0,0 +1,97 @@ +package com.zt.plat.module.databus.controller.admin.gateway; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zt.plat.framework.common.pojo.CommonResult; +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.module.databus.controller.admin.gateway.convert.ApiVersionConvert; +import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSaveReqVO; +import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionCompareRespVO; +import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionDetailRespVO; +import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionPageReqVO; +import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionRespVO; +import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionRollbackReqVO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiVersionDO; +import com.zt.plat.module.databus.service.gateway.ApiVersionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static com.zt.plat.framework.common.pojo.CommonResult.success; + +/** + * API 版本历史控制器。 + */ +@Tag(name = "管理后台 - API 版本历史") +@RestController +@RequestMapping("/databus/gateway/version") +@Validated +@Slf4j +public class ApiVersionController { + + @Resource + private ApiVersionService apiVersionService; + + @Resource + private ObjectMapper objectMapper; + + @GetMapping("/get") + @Operation(summary = "获取 API 版本详情") + @Parameter(name = "id", description = "版本编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('databus:gateway:version:query')") + public CommonResult getVersion(@RequestParam("id") Long id) { + ApiVersionDO versionDO = apiVersionService.getVersion(id); + ApiVersionDetailRespVO respVO = ApiVersionConvert.INSTANCE.convertDetail(versionDO); + + // 反序列化快照数据 + if (versionDO.getSnapshotData() != null) { + try { + ApiDefinitionSaveReqVO snapshot = objectMapper.readValue(versionDO.getSnapshotData(), ApiDefinitionSaveReqVO.class); + respVO.setSnapshotData(snapshot); + } catch (JsonProcessingException ex) { + log.error("反序列化版本快照失败, versionId={}", id, ex); + } + } + + return success(respVO); + } + + @GetMapping("/page") + @Operation(summary = "分页查询 API 版本列表") + @PreAuthorize("@ss.hasPermission('databus:gateway:version:query')") + public CommonResult> getVersionPage(@Valid ApiVersionPageReqVO pageReqVO) { + PageResult pageResult = apiVersionService.getVersionPage(pageReqVO); + return success(ApiVersionConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/list") + @Operation(summary = "查询指定 API 的全部版本") + @PreAuthorize("@ss.hasPermission('databus:gateway:version:query')") + public CommonResult> getVersionList(@RequestParam("apiId") Long apiId) { + return success(ApiVersionConvert.INSTANCE.convertList(apiVersionService.getVersionListByApiId(apiId))); + } + + @PutMapping("/rollback") + @Operation(summary = "回滚到指定版本") + @PreAuthorize("@ss.hasPermission('databus:gateway:version:rollback')") + public CommonResult rollbackToVersion(@Valid @RequestBody ApiVersionRollbackReqVO reqVO) { + apiVersionService.rollbackToVersion(reqVO.getId(), reqVO.getRemark()); + return success(true); + } + + @GetMapping("/compare") + @Operation(summary = "对比两个版本差异") + @PreAuthorize("@ss.hasPermission('databus:gateway:version:query')") + public CommonResult compareVersions( + @RequestParam("sourceId") Long sourceId, + @RequestParam("targetId") Long targetId) { + return success(apiVersionService.compareVersions(sourceId, targetId)); + } + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiAccessLogConvert.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiAccessLogConvert.java new file mode 100644 index 00000000..23cb9e94 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiAccessLogConvert.java @@ -0,0 +1,29 @@ +package com.zt.plat.module.databus.controller.admin.gateway.convert; + +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog.ApiAccessLogRespVO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface ApiAccessLogConvert { + + ApiAccessLogConvert INSTANCE = Mappers.getMapper(ApiAccessLogConvert.class); + + ApiAccessLogRespVO convert(ApiAccessLogDO bean); + + List convertList(List list); + + default PageResult convertPage(PageResult page) { + if (page == null) { + return PageResult.empty(); + } + PageResult result = new PageResult<>(); + result.setList(convertList(page.getList())); + result.setTotal(page.getTotal()); + return result; + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiDefinitionConvert.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiDefinitionConvert.java index 8725b204..cc2736bd 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiDefinitionConvert.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiDefinitionConvert.java @@ -3,12 +3,10 @@ package com.zt.plat.module.databus.controller.admin.gateway.convert; import cn.hutool.core.collection.CollUtil; import com.zt.plat.framework.common.pojo.PageResult; import com.zt.plat.framework.common.util.object.BeanUtils; -import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionDetailRespVO; -import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionPublicationRespVO; -import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionStepRespVO; -import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSummaryRespVO; -import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionTransformRespVO; +import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.*; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiStepDO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiTransformDO; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiFlowPublication; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition; @@ -101,4 +99,29 @@ public interface ApiDefinitionConvert { return publication == null ? null : BeanUtils.toBean(publication, ApiDefinitionPublicationRespVO.class); } + /** + * 转换步骤列表(DO -> SaveReqVO) + */ + default List convertStepList(List steps) { + if (CollUtil.isEmpty(steps)) { + return new ArrayList<>(); + } + return steps.stream() + .sorted(Comparator.comparing(step -> step.getStepOrder() == null ? Integer.MAX_VALUE : step.getStepOrder())) + .map(step -> BeanUtils.toBean(step, ApiDefinitionStepSaveReqVO.class)) + .collect(Collectors.toList()); + } + + /** + * 转换变换列表(DO -> SaveReqVO) + */ + default List convertTransformList(List transforms) { + if (CollUtil.isEmpty(transforms)) { + return new ArrayList<>(); + } + return transforms.stream() + .map(transform -> BeanUtils.toBean(transform, ApiDefinitionTransformSaveReqVO.class)) + .collect(Collectors.toList()); + } + } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiVersionConvert.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiVersionConvert.java new file mode 100644 index 00000000..685d271e --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiVersionConvert.java @@ -0,0 +1,30 @@ +package com.zt.plat.module.databus.controller.admin.gateway.convert; + +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionDetailRespVO; +import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionRespVO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiVersionDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * API 版本历史 Convert。 + */ +@Mapper +public interface ApiVersionConvert { + + ApiVersionConvert INSTANCE = Mappers.getMapper(ApiVersionConvert.class); + + ApiVersionRespVO convert(ApiVersionDO bean); + + PageResult convertPage(PageResult page); + + List convertList(List list); + + @Mapping(target = "snapshotData", ignore = true) + ApiVersionDetailRespVO convertDetail(ApiVersionDO bean); + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogPageReqVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogPageReqVO.java new file mode 100644 index 00000000..7d012a93 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogPageReqVO.java @@ -0,0 +1,53 @@ +package com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog; + +import com.zt.plat.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.zt.plat.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** + * Databus API 访问日志分页查询 VO。 + */ +@Schema(description = "管理后台 - Databus API 访问日志分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ApiAccessLogPageReqVO extends PageParam { + + @Schema(description = "追踪 ID", example = "c8a3d52f-42c8-4b5d-9e26-8c2cc89f6bb5") + private String traceId; + + @Schema(description = "API 编码", example = "user.query") + private String apiCode; + + @Schema(description = "API 版本", example = "v1") + private String apiVersion; + + @Schema(description = "HTTP 方法", example = "POST") + private String requestMethod; + + @Schema(description = "响应 HTTP 状态", example = "200") + private Integer responseStatus; + + @Schema(description = "访问状态", example = "0") + private Integer status; + + @Schema(description = "客户端 IP", example = "192.168.0.10") + private String clientIp; + + @Schema(description = "租户编号", example = "1") + private Long tenantId; + + @Schema(description = "请求路径", example = "/gateway/api/user/query") + private String requestPath; + + @Schema(description = "请求时间区间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] requestTime; +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogRespVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogRespVO.java new file mode 100644 index 00000000..254bcc1e --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogRespVO.java @@ -0,0 +1,89 @@ +package com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * Databus API 访问日志 Response VO。 + */ +@Schema(description = "管理后台 - Databus API 访问日志 Response VO") +@Data +public class ApiAccessLogRespVO { + + @Schema(description = "日志编号", example = "1024") + private Long id; + + @Schema(description = "追踪 ID", example = "c8a3d52f-42c8-4b5d-9e26-8c2cc89f6bb5") + private String traceId; + + @Schema(description = "API 编码", example = "user.query") + private String apiCode; + + @Schema(description = "API 版本", example = "v1") + private String apiVersion; + + @Schema(description = "HTTP 方法", example = "POST") + private String requestMethod; + + @Schema(description = "请求路径", example = "/gateway/api/user/query") + private String requestPath; + + @Schema(description = "查询参数(JSON)") + private String requestQuery; + + @Schema(description = "请求头(JSON)") + private String requestHeaders; + + @Schema(description = "请求体(JSON)") + private String requestBody; + + @Schema(description = "响应 HTTP 状态", example = "200") + private Integer responseStatus; + + @Schema(description = "响应提示", example = "OK") + private String responseMessage; + + @Schema(description = "响应体(JSON)") + private String responseBody; + + @Schema(description = "访问状态", example = "0") + private Integer status; + + @Schema(description = "错误码", example = "DAT-001") + private String errorCode; + + @Schema(description = "错误信息", example = "API 调用失败") + private String errorMessage; + + @Schema(description = "异常堆栈") + private String exceptionStack; + + @Schema(description = "客户端 IP", example = "192.168.0.10") + private String clientIp; + + @Schema(description = "User-Agent") + private String userAgent; + + @Schema(description = "租户编号", example = "1") + private Long tenantId; + + @Schema(description = "请求耗时(毫秒)", example = "123") + private Long duration; + + @Schema(description = "请求时间") + private LocalDateTime requestTime; + + @Schema(description = "响应时间") + private LocalDateTime responseTime; + + @Schema(description = "执行步骤(JSON)") + private String stepResults; + + @Schema(description = "额外调试信息(JSON)") + private String extra; + + @Schema(description = "创建时间") + private LocalDateTime createTime; +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionCompareRespVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionCompareRespVO.java new file mode 100644 index 00000000..e5b6d692 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionCompareRespVO.java @@ -0,0 +1,72 @@ +package com.zt.plat.module.databus.controller.admin.gateway.vo.version; + +import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSaveReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * API 版本对比 Response VO。 + */ +@Schema(description = "管理后台 - API 版本对比 Response VO") +@Data +public class ApiVersionCompareRespVO { + + @Schema(description = "源版本 ID", example = "1001") + private Long sourceVersionId; + + @Schema(description = "源版本号", example = "2") + private Integer sourceVersionNumber; + + @Schema(description = "源版本描述") + private String sourceDescription; + + @Schema(description = "源版本操作人") + private String sourceOperator; + + @Schema(description = "源版本创建时间") + private LocalDateTime sourceCreateTime; + + @Schema(description = "目标版本 ID", example = "1002") + private Long targetVersionId; + + @Schema(description = "目标版本号", example = "3") + private Integer targetVersionNumber; + + @Schema(description = "目标版本描述") + private String targetDescription; + + @Schema(description = "目标版本操作人") + private String targetOperator; + + @Schema(description = "目标版本创建时间") + private LocalDateTime targetCreateTime; + + @Schema(description = "源版本快照") + private ApiDefinitionSaveReqVO sourceSnapshot; + + @Schema(description = "目标版本快照") + private ApiDefinitionSaveReqVO targetSnapshot; + + @Schema(description = "两者是否完全一致") + private Boolean same; + + @Schema(description = "字段差异列表") + private List differences; + + @Data + public static class FieldDiff { + + @Schema(description = "差异字段路径", example = "/steps[0]/targetEndpoint") + private String path; + + @Schema(description = "源版本值") + private String sourceValue; + + @Schema(description = "目标版本值") + private String targetValue; + } + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionCreateReqVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionCreateReqVO.java new file mode 100644 index 00000000..d13a7996 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionCreateReqVO.java @@ -0,0 +1,26 @@ +package com.zt.plat.module.databus.controller.admin.gateway.vo.version; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * API 版本历史创建 Request VO。 + */ +@Schema(description = "管理后台 - API 版本历史创建 Request VO") +@Data +public class ApiVersionCreateReqVO { + + @Schema(description = "API 定义 ID", required = true, example = "1024") + @NotNull(message = "API 定义 ID 不能为空") + private Long apiId; + + @Schema(description = "版本号", required = true, example = "v1.0.0") + @NotBlank(message = "版本号不能为空") + private String versionNumber; + + @Schema(description = "版本描述", example = "初始版本") + private String description; + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionDetailRespVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionDetailRespVO.java new file mode 100644 index 00000000..2cccfcd0 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionDetailRespVO.java @@ -0,0 +1,47 @@ +package com.zt.plat.module.databus.controller.admin.gateway.vo.version; + +import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSaveReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * API 版本详情 Response VO。 + * 包含完整的 API 定义快照数据。 + */ +@Schema(description = "管理后台 - API 版本详情 Response VO") +@Data +public class ApiVersionDetailRespVO { + + @Schema(description = "主键", example = "1024") + private Long id; + + @Schema(description = "API 定义 ID", example = "1001") + private Long apiId; + + @Schema(description = "版本号", example = "1") + private Integer versionNumber; + + @Schema(description = "版本描述", example = "初始版本") + private String description; + + @Schema(description = "是否为当前版本", example = "true") + private Boolean isCurrent; + + @Schema(description = "操作人", example = "admin") + private String operator; + + @Schema(description = "API 定义快照数据") + private ApiDefinitionSaveReqVO snapshotData; + + @Schema(description = "创建者", example = "admin") + private String creator; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "租户编号", example = "1") + private Long tenantId; + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionPageReqVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionPageReqVO.java new file mode 100644 index 00000000..79e60f4f --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionPageReqVO.java @@ -0,0 +1,33 @@ +package com.zt.plat.module.databus.controller.admin.gateway.vo.version; + +import com.zt.plat.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.zt.plat.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** + * API 版本历史分页查询 VO。 + */ +@Schema(description = "管理后台 - API 版本历史分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ApiVersionPageReqVO extends PageParam { + + @Schema(description = "API 定义 ID", required = true, example = "1024") + private Long apiId; + + @Schema(description = "版本号", example = "1") + private Integer versionNumber; + + @Schema(description = "创建时间区间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionRespVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionRespVO.java new file mode 100644 index 00000000..5ad4859f --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionRespVO.java @@ -0,0 +1,42 @@ +package com.zt.plat.module.databus.controller.admin.gateway.vo.version; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * API 版本历史 Response VO。 + */ +@Schema(description = "管理后台 - API 版本历史 Response VO") +@Data +public class ApiVersionRespVO { + + @Schema(description = "主键", example = "1024") + private Long id; + + @Schema(description = "API 定义 ID", example = "1001") + private Long apiId; + + @Schema(description = "版本号", example = "1") + private Integer versionNumber; + + @Schema(description = "版本描述", example = "初始版本") + private String description; + + @Schema(description = "是否为当前版本", example = "true") + private Boolean isCurrent; + + @Schema(description = "操作人", example = "admin") + private String operator; + + @Schema(description = "创建者", example = "admin") + private String creator; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "租户编号", example = "1") + private Long tenantId; + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionRollbackReqVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionRollbackReqVO.java new file mode 100644 index 00000000..0584a4db --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/version/ApiVersionRollbackReqVO.java @@ -0,0 +1,21 @@ +package com.zt.plat.module.databus.controller.admin.gateway.vo.version; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * API 版本回滚 Request VO。 + */ +@Schema(description = "管理后台 - API 版本回滚 Request VO") +@Data +public class ApiVersionRollbackReqVO { + + @Schema(description = "待回滚的版本 ID", required = true, example = "1024") + @NotNull(message = "版本 ID 不能为空") + private Long id; + + @Schema(description = "回滚备注", example = "回滚到版本 v5") + private String remark; + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiAccessLogDO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiAccessLogDO.java new file mode 100644 index 00000000..9341a211 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiAccessLogDO.java @@ -0,0 +1,140 @@ +package com.zt.plat.module.databus.dal.dataobject.gateway; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.zt.plat.framework.tenant.core.db.TenantBaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * Databus API 访问日志数据对象。 + * + *

用于记录 API 编排网关的请求与响应详情,便于审计与问题排查。

+ */ +@TableName("databus_api_access_log") +@KeySequence("databus_api_access_log_seq") +@Data +@EqualsAndHashCode(callSuper = true) +public class ApiAccessLogDO extends TenantBaseDO { + + /** + * 主键 + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 请求追踪标识,对应 {@link com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext#getRequestId()} + */ + private String traceId; + + /** + * API 编码 + */ + private String apiCode; + + /** + * API 版本 + */ + private String apiVersion; + + /** + * HTTP 方法 + */ + private String requestMethod; + + /** + * 请求路径 + */ + private String requestPath; + + /** + * 查询参数(JSON 字符串) + */ + private String requestQuery; + + /** + * 请求头信息(JSON 字符串) + */ + private String requestHeaders; + + /** + * 请求体(JSON 字符串) + */ + private String requestBody; + + /** + * 响应 HTTP 状态码 + */ + private Integer responseStatus; + + /** + * 响应提示信息 + */ + private String responseMessage; + + /** + * 响应体(JSON 字符串) + */ + private String responseBody; + + /** + * 访问状态:0-成功 1-客户端错误 2-服务端错误 3-未知 + */ + private Integer status; + + /** + * 业务错误码 + */ + private String errorCode; + + /** + * 错误信息 + */ + private String errorMessage; + + /** + * 异常堆栈 + */ + private String exceptionStack; + + /** + * 客户端 IP + */ + private String clientIp; + + /** + * User-Agent + */ + private String userAgent; + + /** + * 请求耗时(毫秒) + */ + private Long duration; + + /** + * 请求时间 + */ + private LocalDateTime requestTime; + + /** + * 响应时间 + */ + private LocalDateTime responseTime; + + /** + * 执行步骤结果(JSON 字符串) + */ + private String stepResults; + + /** + * 额外调试信息(JSON 字符串) + */ + private String extra; + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiVersionDO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiVersionDO.java new file mode 100644 index 00000000..175b8659 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiVersionDO.java @@ -0,0 +1,59 @@ +package com.zt.plat.module.databus.dal.dataobject.gateway; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.zt.plat.framework.tenant.core.db.TenantBaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * API 版本历史数据对象 + * + *

每次修改 API 配置时自动创建新版本记录,支持完整的版本历史追溯和回滚。

+ *

版本号自动递增,不可删除,保留完整的历史记录链。

+ */ +@TableName("databus_api_version") +@KeySequence("databus_api_version_seq") +@Data +@EqualsAndHashCode(callSuper = true) +public class ApiVersionDO extends TenantBaseDO { + + /** + * 主键 + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * API 定义 ID + */ + private Long apiId; + + /** + * 版本号(自动递增,从 1 开始) + */ + private Integer versionNumber; + + /** + * API 完整定义快照(JSON 格式,包含 definition、steps、transforms 等) + */ + private String snapshotData; + + /** + * 版本描述/变更说明 + */ + private String description; + + /** + * 是否为当前版本(最新使用的版本) + */ + private Boolean isCurrent; + + /** + * 操作人 + */ + private String operator; + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mapper/gateway/ApiVersionMapper.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mapper/gateway/ApiVersionMapper.java new file mode 100644 index 00000000..e9800a96 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mapper/gateway/ApiVersionMapper.java @@ -0,0 +1,78 @@ +package com.zt.plat.module.databus.dal.mapper.gateway; + +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX; +import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionPageReqVO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiVersionDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * API 版本历史 Mapper + */ +@Mapper +public interface ApiVersionMapper extends BaseMapperX { + + /** + * 分页查询版本历史 + */ + default PageResult selectPage(ApiVersionPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(ApiVersionDO::getApiId, reqVO.getApiId()) + .eqIfPresent(ApiVersionDO::getVersionNumber, reqVO.getVersionNumber()) + .betweenIfPresent(ApiVersionDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(ApiVersionDO::getVersionNumber)); + } + + /** + * 查询指定 API 的所有版本历史 + */ + default List selectListByApiId(Long apiId) { + return selectList(new LambdaQueryWrapperX() + .eq(ApiVersionDO::getApiId, apiId) + .orderByDesc(ApiVersionDO::getVersionNumber)); + } + + /** + * 查询指定 API 的当前版本 + */ + default ApiVersionDO selectCurrentByApiId(Long apiId) { + return selectOne(new LambdaQueryWrapperX() + .eq(ApiVersionDO::getApiId, apiId) + .eq(ApiVersionDO::getIsCurrent, true)); + } + + /** + * 查询指定 API 的最大版本号 + */ + default Integer selectMaxVersionNumber(Long apiId) { + ApiVersionDO maxVersion = selectOne(new LambdaQueryWrapperX() + .eq(ApiVersionDO::getApiId, apiId) + .orderByDesc(ApiVersionDO::getVersionNumber) + .last("LIMIT 1")); + return maxVersion != null ? maxVersion.getVersionNumber() : 0; + } + + /** + * 将所有版本标记为非当前版本 + */ + default void markAllAsNotCurrent(Long apiId) { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.eq("api_id", apiId) + .set("is_current", false); + update(null, updateWrapper); + } + + /** + * 查询指定版本 + */ + default ApiVersionDO selectByApiIdAndVersionNumber(Long apiId, Integer versionNumber) { + return selectOne(new LambdaQueryWrapperX() + .eq(ApiVersionDO::getApiId, apiId) + .eq(ApiVersionDO::getVersionNumber, versionNumber)); + } + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiAccessLogMapper.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiAccessLogMapper.java new file mode 100644 index 00000000..8d58239b --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiAccessLogMapper.java @@ -0,0 +1,31 @@ +package com.zt.plat.module.databus.dal.mysql.gateway; + +import cn.hutool.core.util.ArrayUtil; +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX; +import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog.ApiAccessLogPageReqVO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ApiAccessLogMapper extends BaseMapperX { + + default PageResult selectPage(ApiAccessLogPageReqVO reqVO) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .likeIfPresent(ApiAccessLogDO::getTraceId, reqVO.getTraceId()) + .eqIfPresent(ApiAccessLogDO::getApiCode, reqVO.getApiCode()) + .eqIfPresent(ApiAccessLogDO::getApiVersion, reqVO.getApiVersion()) + .eqIfPresent(ApiAccessLogDO::getRequestMethod, reqVO.getRequestMethod()) + .eqIfPresent(ApiAccessLogDO::getResponseStatus, reqVO.getResponseStatus()) + .eqIfPresent(ApiAccessLogDO::getStatus, reqVO.getStatus()) + .likeIfPresent(ApiAccessLogDO::getClientIp, reqVO.getClientIp()) + .eqIfPresent(ApiAccessLogDO::getTenantId, reqVO.getTenantId()) + .likeIfPresent(ApiAccessLogDO::getRequestPath, reqVO.getRequestPath()); + if (ArrayUtil.isNotEmpty(reqVO.getRequestTime()) && reqVO.getRequestTime().length == 2) { + query.between(ApiAccessLogDO::getRequestTime, reqVO.getRequestTime()[0], reqVO.getRequestTime()[1]); + } + return selectPage(reqVO, query.orderByDesc(ApiAccessLogDO::getRequestTime) + .orderByDesc(ApiAccessLogDO::getId)); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java new file mode 100644 index 00000000..56cd6f2d --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java @@ -0,0 +1,255 @@ +package com.zt.plat.module.databus.framework.integration.gateway.core; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; +import com.zt.plat.module.databus.service.gateway.ApiAccessLogService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.HashMap; +import java.util.Map; + +/** + * 将 API 调用上下文持久化为访问日志。 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ApiGatewayAccessLogger { + + public static final String ATTR_LOG_ID = "ApiAccessLogId"; + public static final String ATTR_EXCEPTION_STACK = "ApiAccessLogExceptionStack"; + + private static final int MAX_TEXT_LENGTH = 4000; + + private final ApiAccessLogService apiAccessLogService; + private final ObjectMapper objectMapper; + + /** + * 在分发前记录请求信息。 + */ + public void onRequest(ApiInvocationContext context) { + try { + ApiAccessLogDO logDO = new ApiAccessLogDO(); + logDO.setTraceId(context.getRequestId()); + logDO.setApiCode(context.getApiCode()); + logDO.setApiVersion(context.getApiVersion()); + logDO.setRequestMethod(context.getHttpMethod()); + logDO.setRequestPath(context.getRequestPath()); + logDO.setRequestQuery(toJson(context.getRequestQueryParams())); + logDO.setRequestHeaders(toJson(context.getRequestHeaders())); + logDO.setRequestBody(toJson(context.getRequestBody())); + logDO.setClientIp(firstNonBlank(context.getClientIp(), + GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), "X-Forwarded-For"))); + logDO.setUserAgent(GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HttpHeaders.USER_AGENT)); + logDO.setStatus(3); // 默认未知 + logDO.setRequestTime(toLocalDateTime(context.getRequestTime())); + logDO.setTenantId(parseTenantId(context.getTenantId())); + Long logId = apiAccessLogService.create(logDO); + context.getAttributes().put(ATTR_LOG_ID, logId); + } catch (Exception ex) { + log.warn("记录 API 访问日志开始阶段失败, traceId={}", context.getRequestId(), ex); + } + } + + /** + * 记录异常堆栈,便于后续写入日志。 + */ + public void onException(ApiInvocationContext context, Throwable throwable) { + if (throwable == null) { + return; + } + context.getAttributes().put(ATTR_EXCEPTION_STACK, buildStackTrace(throwable)); + } + + /** + * 在分发完成后补全日志信息。 + */ + public void onResponse(ApiInvocationContext context) { + Long logId = getLogId(context); + if (logId == null) { + return; + } + try { + ApiAccessLogDO update = new ApiAccessLogDO(); + update.setId(logId); + update.setResponseStatus(context.getResponseStatus()); + update.setResponseMessage(context.getResponseMessage()); + update.setResponseBody(toJson(context.getResponseBody())); + update.setStatus(resolveStatus(context.getResponseStatus())); + update.setErrorCode(extractErrorCode(context.getResponseBody())); + update.setErrorMessage(resolveErrorMessage(context)); + update.setExceptionStack((String) context.getAttributes().get(ATTR_EXCEPTION_STACK)); + update.setStepResults(toJson(context.getStepResults())); + update.setExtra(toJson(buildExtra(context))); + update.setResponseTime(LocalDateTime.now()); + update.setDuration(calculateDuration(context)); + apiAccessLogService.update(update); + } catch (Exception ex) { + log.warn("记录 API 访问日志结束阶段失败, traceId={}, logId={}", context.getRequestId(), logId, ex); + } + } + + private Long getLogId(ApiInvocationContext context) { + Object value = context.getAttributes().get(ATTR_LOG_ID); + if (value instanceof Long) { + return (Long) value; + } + if (value instanceof Number number) { + return number.longValue(); + } + return null; + } + + private Long calculateDuration(ApiInvocationContext context) { + Instant start = context.getRequestTime(); + if (start == null) { + return null; + } + return Duration.between(start, Instant.now()).toMillis(); + } + + private Integer resolveStatus(Integer httpStatus) { + if (httpStatus == null) { + return 3; + } + if (httpStatus >= 200 && httpStatus < 400) { + return 0; + } + if (httpStatus >= 400 && httpStatus < 500) { + return 1; + } + if (httpStatus >= 500) { + return 2; + } + return 3; + } + + private String resolveErrorMessage(ApiInvocationContext context) { + if (StringUtils.hasText(context.getResponseMessage())) { + return truncate(context.getResponseMessage()); + } + Object responseBody = context.getResponseBody(); + if (responseBody instanceof Map map) { + Object message = firstNonNull(map.get("errorMessage"), map.get("message")); + if (message != null) { + return truncate(String.valueOf(message)); + } + } + return null; + } + + private String extractErrorCode(Object responseBody) { + if (responseBody instanceof Map map) { + Object errorCode = firstNonNull(map.get("errorCode"), map.get("code")); + return errorCode == null ? null : truncate(String.valueOf(errorCode)); + } + return null; + } + + private Map buildExtra(ApiInvocationContext context) { + Map extra = new HashMap<>(); + if (!CollectionUtils.isEmpty(context.getVariables())) { + extra.put("variables", context.getVariables()); + } + if (!CollectionUtils.isEmpty(context.getAttributes())) { + Map attributes = new HashMap<>(context.getAttributes()); + attributes.remove(ATTR_LOG_ID); + attributes.remove(ATTR_EXCEPTION_STACK); + if (!attributes.isEmpty()) { + extra.put("attributes", attributes); + } + } + if (CollectionUtils.isEmpty(extra)) { + return null; + } + return extra; + } + + private String toJson(Object value) { + if (value == null) { + return null; + } + if (value instanceof String str) { + return truncate(str); + } + try { + return truncate(objectMapper.writeValueAsString(value)); + } catch (JsonProcessingException ex) { + return truncate(String.valueOf(value)); + } + } + + private String truncate(String text) { + if (!StringUtils.hasText(text)) { + return text; + } + if (text.length() <= MAX_TEXT_LENGTH) { + return text; + } + return text.substring(0, MAX_TEXT_LENGTH); + } + + private Long parseTenantId(String tenantId) { + if (!StringUtils.hasText(tenantId)) { + return null; + } + try { + return Long.parseLong(tenantId.trim()); + } catch (NumberFormatException ex) { + return null; + } + } + + private LocalDateTime toLocalDateTime(Instant instant) { + if (instant == null) { + return null; + } + return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + } + + private String buildStackTrace(Throwable throwable) { + try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) { + throwable.printStackTrace(pw); + return truncate(sw.toString()); + } catch (Exception ex) { + return throwable.getMessage(); + } + } + + private String firstNonBlank(String... values) { + if (values == null) { + return null; + } + for (String value : values) { + if (StringUtils.hasText(value)) { + return value; + } + } + return null; + } + + private Object firstNonNull(Object... values) { + if (values == null) { + return null; + } + for (Object value : values) { + if (value != null) { + return value; + } + } + return null; + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionService.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionService.java index d84dea8d..1c593897 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionService.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionService.java @@ -38,12 +38,15 @@ public class ApiGatewayExecutionService { private static final String HEADER_REQUEST_URI = org.springframework.integration.http.HttpHeaders.PREFIX + "requestUri"; private static final String HEADER_REQUEST_PARAMS = org.springframework.integration.http.HttpHeaders.PREFIX + "requestParams"; private static final String HEADER_QUERY_STRING = org.springframework.integration.http.HttpHeaders.PREFIX + "queryString"; + private static final String HEADER_REMOTE_ADDRESS = org.springframework.integration.http.HttpHeaders.PREFIX + "remoteAddress"; + private static final String LOCAL_DEBUG_REMOTE_ADDRESS = "127.0.0.1"; private final ApiGatewayRequestMapper requestMapper; private final ApiFlowDispatcher apiFlowDispatcher; private final ApiGatewayErrorProcessor errorProcessor; private final ApiGatewayProperties properties; private final ObjectMapper objectMapper; + private final ApiGatewayAccessLogger accessLogger; /** * Maps a raw HTTP message (as provided by Spring Integration) into a context message. @@ -62,26 +65,34 @@ public class ApiGatewayExecutionService { */ public ApiInvocationContext dispatch(Message message) { ApiInvocationContext context = message.getPayload(); + accessLogger.onRequest(context); + ApiInvocationContext responseContext; try { - return apiFlowDispatcher.dispatch(context.getApiCode(), context.getApiVersion(), context); + responseContext = apiFlowDispatcher.dispatch(context.getApiCode(), context.getApiVersion(), context); } catch (ServiceException ex) { errorProcessor.applyServiceException(context, ex); + accessLogger.onException(context, ex); log.warn("[API-PORTAL] 分发 apiCode={} version={} 时出现 ServiceException: {}", context.getApiCode(), context.getApiVersion(), ex.getMessage()); - return context; + responseContext = context; } catch (Exception ex) { ServiceException nestedServiceException = errorProcessor.resolveServiceException(ex); if (nestedServiceException != null) { errorProcessor.applyServiceException(context, nestedServiceException); + accessLogger.onException(context, nestedServiceException); log.warn("[API-PORTAL] 分发 apiCode={} version={} 时出现 ServiceException(包装异常): {}", context.getApiCode(), context.getApiVersion(), nestedServiceException.getMessage()); if (log.isDebugEnabled()) { log.debug("[API-PORTAL] 包装异常堆栈", ex); } } else { errorProcessor.applyUnexpectedException(context, ex); + accessLogger.onException(context, ex); log.error("[API-PORTAL] 分发 apiCode={} version={} 时出现未预期异常", context.getApiCode(), context.getApiVersion(), ex); } - return context; + responseContext = context; + } finally { + accessLogger.onResponse(context); } + return responseContext; } /** @@ -115,7 +126,7 @@ public class ApiGatewayExecutionService { "version", reqVO.getVersion() ); builder.setHeader(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriVariables); - builder.setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_METHOD, HttpMethod.POST.name()); + builder.setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_METHOD, HttpMethod.POST.name()); String basePath = normalizeBasePath(properties.getBasePath()); String rawQuery = buildQueryString(reqVO.getQueryParams()); @@ -125,10 +136,11 @@ public class ApiGatewayExecutionService { } builder.setHeader(HEADER_REQUEST_URI, requestUri); builder.setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_URL, requestUri); + builder.setHeader(HEADER_REMOTE_ADDRESS, LOCAL_DEBUG_REMOTE_ADDRESS); Map requestHeaders = new LinkedHashMap<>(); if (reqVO.getHeaders() != null) { - reqVO.getHeaders().forEach(requestHeaders::put); + requestHeaders.putAll(reqVO.getHeaders()); } normalizeJwtHeaders(requestHeaders, reqVO.getQueryParams()); requestHeaders.putIfAbsent(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapper.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapper.java index efce57c7..fadfe510 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapper.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapper.java @@ -33,6 +33,7 @@ public class ApiGatewayRequestMapper { private static final String HEADER_REQUEST_HEADERS = org.springframework.integration.http.HttpHeaders.PREFIX + "requestHeaders"; private static final String HEADER_REQUEST_URI = org.springframework.integration.http.HttpHeaders.PREFIX + "requestUri"; + private static final String HEADER_REMOTE_ADDRESS = org.springframework.integration.http.HttpHeaders.PREFIX + "remoteAddress"; @SuppressWarnings("unchecked") public ApiInvocationContext map(Object payload, Map headers) { @@ -87,6 +88,8 @@ public class ApiGatewayRequestMapper { context.getRequestHeaders().putIfAbsent(key, normalized); } }); + context.setUserAgent(GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HttpHeaders.USER_AGENT)); + context.setClientIp(resolveClientIp(headers, context.getRequestHeaders())); populateQueryParams(headers, context, originalRequestUri); if (properties.isEnableTenantHeader()) { Object tenantHeaderValue = context.getRequestHeaders().get(properties.getTenantHeader()); @@ -315,4 +318,26 @@ public class ApiGatewayRequestMapper { } return candidate; } + + private String resolveClientIp(Map headers, Map requestHeaders) { + String forwarded = GatewayHeaderUtils.findFirstHeaderValue(requestHeaders, "X-Forwarded-For"); + if (StringUtils.hasText(forwarded)) { + int idx = forwarded.indexOf(','); + return idx >= 0 ? forwarded.substring(0, idx).trim() : forwarded; + } + String realIp = GatewayHeaderUtils.findFirstHeaderValue(requestHeaders, "X-Real-IP"); + if (StringUtils.hasText(realIp)) { + return realIp; + } + Object remote = headers.get(HEADER_REMOTE_ADDRESS); + if (remote != null) { + String candidate = remote.toString(); + int slash = candidate.indexOf('/') >= 0 ? candidate.indexOf('/') : candidate.indexOf(':'); + if (slash > 0) { + candidate = candidate.substring(0, slash); + } + return candidate.trim(); + } + return null; + } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/model/ApiInvocationContext.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/model/ApiInvocationContext.java index dc1840bb..a17dc89f 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/model/ApiInvocationContext.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/model/ApiInvocationContext.java @@ -25,6 +25,10 @@ public class ApiInvocationContext { private String tenantId; + private String clientIp; + + private String userAgent; + private String httpMethod; private String requestPath; @@ -66,6 +70,8 @@ public class ApiInvocationContext { copy.apiCode = this.apiCode; copy.apiVersion = this.apiVersion; copy.tenantId = this.tenantId; + copy.clientIp = this.clientIp; + copy.userAgent = this.userAgent; copy.httpMethod = this.httpMethod; copy.requestPath = this.requestPath; copy.requestBody = this.requestBody; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiAccessLogService.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiAccessLogService.java new file mode 100644 index 00000000..fc43171f --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiAccessLogService.java @@ -0,0 +1,42 @@ +package com.zt.plat.module.databus.service.gateway; + +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog.ApiAccessLogPageReqVO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO; + +/** + * Databus API 访问日志 Service。 + */ +public interface ApiAccessLogService { + + /** + * 新增访问日志。 + * + * @param logDO 日志信息 + * @return 日志编号 + */ + Long create(ApiAccessLogDO logDO); + + /** + * 更新访问日志(仅更新非空字段)。 + * + * @param logDO 日志信息 + */ + void update(ApiAccessLogDO logDO); + + /** + * 根据编号获取访问日志。 + * + * @param id 日志编号 + * @return 日志 + */ + ApiAccessLogDO get(Long id); + + /** + * 分页查询访问日志。 + * + * @param pageReqVO 查询条件 + * @return 日志分页 + */ + PageResult getPage(ApiAccessLogPageReqVO pageReqVO); +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiVersionService.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiVersionService.java new file mode 100644 index 00000000..8ee1c025 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiVersionService.java @@ -0,0 +1,65 @@ +package com.zt.plat.module.databus.service.gateway; + +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionCompareRespVO; +import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionPageReqVO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiVersionDO; +import java.util.List; + +/** + * API 版本历史 Service 接口(备忘录模式)。 + */ +public interface ApiVersionService { + + /** + * 捕获当前 API 配置快照并生成版本记录。 + * + * @param apiId API ID + * @param description 版本描述(可为空) + * @param operator 操作人 + * @return 版本 ID + */ + Long autoCreateVersion(Long apiId, String description, String operator); + + /** + * 查询版本详情。 + * + * @param id 版本 ID + * @return 版本信息 + */ + ApiVersionDO getVersion(Long id); + + /** + * 查询指定 API 的版本历史(分页)。 + * + * @param pageReqVO 分页参数 + * @return 版本列表 + */ + PageResult getVersionPage(ApiVersionPageReqVO pageReqVO); + + /** + * 查询指定 API 的全部版本历史(倒序)。 + * + * @param apiId API ID + * @return 版本列表 + */ + List getVersionListByApiId(Long apiId); + + /** + * 回滚至指定版本。 + * + * @param id 版本 ID + * @param remark 回滚说明(可为空) + */ + void rollbackToVersion(Long id, String remark); + + /** + * 对比两个版本的差异。 + * + * @param sourceVersionId 源版本 ID + * @param targetVersionId 目标版本 ID + * @return 差异结果 + */ + ApiVersionCompareRespVO compareVersions(Long sourceVersionId, Long targetVersionId); + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiVersionSnapshotContextHolder.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiVersionSnapshotContextHolder.java new file mode 100644 index 00000000..1e5d2f3f --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiVersionSnapshotContextHolder.java @@ -0,0 +1,33 @@ +package com.zt.plat.module.databus.service.gateway; + +/** + * Thread-local context to control whether API definition updates should capture version snapshots. + */ +public final class ApiVersionSnapshotContextHolder { + + private static final ThreadLocal SKIP_SNAPSHOT = new ThreadLocal<>(); + + private ApiVersionSnapshotContextHolder() { + } + + /** + * Mark that the current thread should skip automatic version snapshot creation once. + */ + public static void markSkipOnce() { + SKIP_SNAPSHOT.set(Boolean.TRUE); + } + + /** + * Determine whether the current thread requested to skip snapshot creation. + */ + public static boolean shouldSkip() { + return Boolean.TRUE.equals(SKIP_SNAPSHOT.get()); + } + + /** + * Clear the skip flag from the current thread. + */ + public static void clear() { + SKIP_SNAPSHOT.remove(); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiAccessLogServiceImpl.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiAccessLogServiceImpl.java new file mode 100644 index 00000000..5a3a8e23 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiAccessLogServiceImpl.java @@ -0,0 +1,47 @@ +package com.zt.plat.module.databus.service.gateway.impl; + +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog.ApiAccessLogPageReqVO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO; +import com.zt.plat.module.databus.dal.mysql.gateway.ApiAccessLogMapper; +import com.zt.plat.module.databus.service.gateway.ApiAccessLogService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +/** + * Databus API 访问日志 Service 实现。 + */ +@Service +@Validated +@Slf4j +public class ApiAccessLogServiceImpl implements ApiAccessLogService { + + @Resource + private ApiAccessLogMapper apiAccessLogMapper; + + @Override + public Long create(ApiAccessLogDO logDO) { + apiAccessLogMapper.insert(logDO); + return logDO.getId(); + } + + @Override + public void update(ApiAccessLogDO logDO) { + int rows = apiAccessLogMapper.updateById(logDO); + if (rows == 0 && log.isDebugEnabled()) { + log.debug("访问日志不存在,无法更新。id={}", logDO.getId()); + } + } + + @Override + public ApiAccessLogDO get(Long id) { + return apiAccessLogMapper.selectById(id); + } + + @Override + public PageResult getPage(ApiAccessLogPageReqVO pageReqVO) { + return apiAccessLogMapper.selectPage(pageReqVO); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiDefinitionServiceImpl.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiDefinitionServiceImpl.java index 894ee3cb..c7ca8d51 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiDefinitionServiceImpl.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiDefinitionServiceImpl.java @@ -9,31 +9,27 @@ import com.github.benmanes.caffeine.cache.LoadingCache; import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; import com.zt.plat.framework.common.pojo.PageResult; import com.zt.plat.framework.common.util.object.BeanUtils; +import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils; import com.zt.plat.framework.tenant.core.context.TenantContextHolder; +import com.zt.plat.framework.tenant.core.db.TenantBaseDO; import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionPageReqVO; import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSaveReqVO; import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionStepSaveReqVO; import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionTransformSaveReqVO; -import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO; -import com.zt.plat.module.databus.dal.dataobject.gateway.ApiFlowPublishDO; -import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO; -import com.zt.plat.module.databus.dal.dataobject.gateway.ApiStepDO; -import com.zt.plat.module.databus.dal.dataobject.gateway.ApiTransformDO; -import com.zt.plat.module.databus.dal.mysql.gateway.ApiDefinitionMapper; -import com.zt.plat.module.databus.dal.mysql.gateway.ApiFlowPublishMapper; -import com.zt.plat.module.databus.dal.mysql.gateway.ApiPolicyRateLimitMapper; -import com.zt.plat.module.databus.dal.mysql.gateway.ApiStepMapper; -import com.zt.plat.module.databus.dal.mysql.gateway.ApiTransformMapper; +import com.zt.plat.module.databus.dal.dataobject.gateway.*; +import com.zt.plat.module.databus.dal.mysql.gateway.*; import com.zt.plat.module.databus.enums.gateway.ApiStatusEnum; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiFlowPublication; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiTransformDefinition; import com.zt.plat.module.databus.service.gateway.ApiDefinitionService; -import com.zt.plat.framework.tenant.core.db.TenantBaseDO; +import com.zt.plat.module.databus.service.gateway.ApiVersionService; +import com.zt.plat.module.databus.service.gateway.ApiVersionSnapshotContextHolder; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; @@ -61,6 +57,7 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { private final ApiFlowPublishMapper apiFlowPublishMapper; private final ObjectMapper objectMapper; private final StringRedisTemplate stringRedisTemplate; + private final ObjectProvider apiVersionServiceProvider; private LoadingCache> definitionCache; @@ -133,6 +130,10 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { persistApiLevelTransforms(apiId, reqVO.getApiLevelTransforms()); persistSteps(apiId, reqVO.getSteps()); + + String operator = SecurityFrameworkUtils.getLoginUserNickname(); + String description = String.format("创建 API (%s)", reqVO.getVersion()); + apiVersionServiceProvider.getObject().autoCreateVersion(apiId, description, operator); return apiId; } @@ -141,19 +142,35 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { public void update(ApiDefinitionSaveReqVO reqVO) { ApiDefinitionDO existing = ensureExists(reqVO.getId()); - validateDuplication(reqVO, existing.getId()); - validateStructure(reqVO); - validatePolicies(reqVO); + boolean skipSnapshot = ApiVersionSnapshotContextHolder.shouldSkip(); - ApiDefinitionDO updateObj = buildDefinitionDO(reqVO, existing); - apiDefinitionMapper.updateById(updateObj); + try { + validateDuplication(reqVO, existing.getId()); + validateStructure(reqVO); + validatePolicies(reqVO); - invalidateCache(existing.getTenantId(), existing.getApiCode(), existing.getVersion()); - apiTransformMapper.deleteByApiId(existing.getId()); - apiStepMapper.deleteByApiId(existing.getId()); - persistApiLevelTransforms(existing.getId(), reqVO.getApiLevelTransforms()); - persistSteps(existing.getId(), reqVO.getSteps()); - invalidateCache(updateObj.getTenantId(), updateObj.getApiCode(), updateObj.getVersion()); + ApiDefinitionDO updateObj = buildDefinitionDO(reqVO, existing); + apiDefinitionMapper.updateById(updateObj); + + invalidateCache(existing.getTenantId(), existing.getApiCode(), existing.getVersion()); + apiTransformMapper.deleteByApiId(existing.getId()); + apiStepMapper.deleteByApiId(existing.getId()); + persistApiLevelTransforms(existing.getId(), reqVO.getApiLevelTransforms()); + persistSteps(existing.getId(), reqVO.getSteps()); + invalidateCache(updateObj.getTenantId(), updateObj.getApiCode(), updateObj.getVersion()); + } finally { + if (skipSnapshot) { + ApiVersionSnapshotContextHolder.clear(); + } + } + + if (skipSnapshot) { + return; + } + + String operator = SecurityFrameworkUtils.getLoginUserNickname(); + String description = String.format("更新 API (%s)", reqVO.getVersion()); + apiVersionServiceProvider.getObject().autoCreateVersion(existing.getId(), description, operator); } @Override diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiVersionServiceImpl.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiVersionServiceImpl.java new file mode 100644 index 00000000..f4961fa1 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiVersionServiceImpl.java @@ -0,0 +1,299 @@ +package com.zt.plat.module.databus.service.gateway.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils; +import com.zt.plat.module.databus.controller.admin.gateway.convert.ApiDefinitionConvert; +import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSaveReqVO; +import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionCompareRespVO; +import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionPageReqVO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiStepDO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiTransformDO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiVersionDO; +import com.zt.plat.module.databus.dal.mapper.gateway.ApiVersionMapper; +import com.zt.plat.module.databus.dal.mysql.gateway.ApiDefinitionMapper; +import com.zt.plat.module.databus.dal.mysql.gateway.ApiStepMapper; +import com.zt.plat.module.databus.dal.mysql.gateway.ApiTransformMapper; +import com.zt.plat.module.databus.service.gateway.ApiDefinitionService; +import com.zt.plat.module.databus.service.gateway.ApiVersionService; +import com.zt.plat.module.databus.service.gateway.ApiVersionSnapshotContextHolder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.*; + +/** + * API 版本历史 Service 实现类(备忘录模式)。 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ApiVersionServiceImpl implements ApiVersionService { + + private final ApiVersionMapper apiVersionMapper; + private final ApiDefinitionMapper apiDefinitionMapper; + private final ApiStepMapper apiStepMapper; + private final ApiTransformMapper apiTransformMapper; + private final ObjectMapper objectMapper; + private final ApiDefinitionService apiDefinitionService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long autoCreateVersion(Long apiId, String description, String operator) { + ApiDefinitionDO definition = ensureApiExists(apiId); + + ApiDefinitionSaveReqVO snapshot = buildApiSnapshot(definition); + String snapshotJson = serializeSnapshot(apiId, snapshot); + + ApiVersionDO currentVersion = apiVersionMapper.selectCurrentByApiId(apiId); + if (currentVersion != null && snapshotEquals(currentVersion.getSnapshotData(), snapshotJson)) { + log.debug("[API-VERSION] Skip creating snapshot, apiId={} current version remains {}", apiId, currentVersion.getVersionNumber()); + return currentVersion.getId(); + } + + Integer maxVersionNumber = apiVersionMapper.selectMaxVersionNumber(apiId); + int nextVersionNumber = (maxVersionNumber == null ? 0 : maxVersionNumber) + 1; + + apiVersionMapper.markAllAsNotCurrent(apiId); + + ApiVersionDO version = new ApiVersionDO(); + version.setApiId(apiId); + version.setVersionNumber(nextVersionNumber); + version.setDescription(StringUtils.hasText(description) ? description : defaultDescription(nextVersionNumber)); + version.setIsCurrent(Boolean.TRUE); + version.setOperator(StringUtils.hasText(operator) ? operator : "system"); + version.setSnapshotData(snapshotJson); + apiVersionMapper.insert(version); + + log.info("[API-VERSION] Created snapshot apiId={} version=v{} versionId={}", apiId, nextVersionNumber, version.getId()); + return version.getId(); + } + + @Override + public ApiVersionDO getVersion(Long id) { + ApiVersionDO version = apiVersionMapper.selectById(id); + if (version == null) { + throw ServiceExceptionUtil.exception(API_VERSION_NOT_FOUND); + } + return version; + } + + @Override + public PageResult getVersionPage(ApiVersionPageReqVO pageReqVO) { + return apiVersionMapper.selectPage(pageReqVO); + } + + @Override + public List getVersionListByApiId(Long apiId) { + ensureApiExists(apiId); + return apiVersionMapper.selectListByApiId(apiId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void rollbackToVersion(Long id, String remark) { + ApiVersionDO targetVersion = getVersion(id); + ApiDefinitionSaveReqVO snapshot = deserializeSnapshot(targetVersion); + + ensureApiExists(targetVersion.getApiId()); + + ApiVersionSnapshotContextHolder.markSkipOnce(); + try { + apiDefinitionService.update(snapshot); + } finally { + ApiVersionSnapshotContextHolder.clear(); + } + + String operator = SecurityFrameworkUtils.getLoginUserNickname(); + String description = StringUtils.hasText(remark) + ? remark + : String.format("回滚到 v%d", targetVersion.getVersionNumber()); + autoCreateVersion(targetVersion.getApiId(), description, operator); + log.info("[API-VERSION] Rolled back apiId={} to version v{} and created new snapshot", targetVersion.getApiId(), targetVersion.getVersionNumber()); + } + + @Override + public ApiVersionCompareRespVO compareVersions(Long sourceVersionId, Long targetVersionId) { + ApiVersionDO source = getVersion(sourceVersionId); + ApiVersionDO target = getVersion(targetVersionId); + + if (!Objects.equals(source.getApiId(), target.getApiId())) { + throw ServiceExceptionUtil.exception(API_VERSION_API_MISMATCH); + } + + ApiDefinitionSaveReqVO sourceSnapshot = deserializeSnapshot(source); + ApiDefinitionSaveReqVO targetSnapshot = deserializeSnapshot(target); + + JsonNode sourceNode = readTree(source.getSnapshotData()); + JsonNode targetNode = readTree(target.getSnapshotData()); + + List differences = new ArrayList<>(); + collectDifferences(sourceNode, targetNode, "", differences); + + ApiVersionCompareRespVO respVO = new ApiVersionCompareRespVO(); + respVO.setSourceVersionId(source.getId()); + respVO.setSourceVersionNumber(source.getVersionNumber()); + respVO.setSourceDescription(source.getDescription()); + respVO.setSourceOperator(source.getOperator()); + respVO.setSourceCreateTime(source.getCreateTime()); + respVO.setTargetVersionId(target.getId()); + respVO.setTargetVersionNumber(target.getVersionNumber()); + respVO.setTargetDescription(target.getDescription()); + respVO.setTargetOperator(target.getOperator()); + respVO.setTargetCreateTime(target.getCreateTime()); + respVO.setSourceSnapshot(sourceSnapshot); + respVO.setTargetSnapshot(targetSnapshot); + respVO.setSame(differences.isEmpty()); + respVO.setDifferences(differences); + return respVO; + } + + private ApiDefinitionDO ensureApiExists(Long apiId) { + ApiDefinitionDO definition = apiDefinitionMapper.selectById(apiId); + if (definition == null) { + throw ServiceExceptionUtil.exception(API_DEFINITION_NOT_FOUND); + } + return definition; + } + + private ApiDefinitionSaveReqVO buildApiSnapshot(ApiDefinitionDO definition) { + Long apiId = definition.getId(); + ApiDefinitionSaveReqVO snapshot = new ApiDefinitionSaveReqVO(); + snapshot.setId(definition.getId()); + snapshot.setApiCode(definition.getApiCode()); + snapshot.setVersion(definition.getVersion()); + snapshot.setHttpMethod(definition.getHttpMethod()); + snapshot.setStatus(definition.getStatus()); + snapshot.setDescription(definition.getDescription()); + snapshot.setRateLimitId(definition.getRateLimitId()); + snapshot.setResponseTemplate(definition.getResponseTemplate()); + + List steps = apiStepMapper.selectByApiId(apiId); + if (steps != null && !steps.isEmpty()) { + snapshot.setSteps(ApiDefinitionConvert.INSTANCE.convertStepList(steps)); + } + + List apiTransforms = apiTransformMapper.selectApiLevelTransforms(apiId); + if (apiTransforms != null && !apiTransforms.isEmpty()) { + snapshot.setApiLevelTransforms(ApiDefinitionConvert.INSTANCE.convertTransformList(apiTransforms)); + } + + return snapshot; + } + + private String serializeSnapshot(Long apiId, ApiDefinitionSaveReqVO snapshot) { + try { + return objectMapper.writeValueAsString(snapshot); + } catch (JsonProcessingException ex) { + log.error("[API-VERSION] Failed to serialize snapshot, apiId={}", apiId, ex); + throw ServiceExceptionUtil.exception(API_VERSION_SNAPSHOT_SERIALIZE_FAILED); + } + } + + private ApiDefinitionSaveReqVO deserializeSnapshot(ApiVersionDO version) { + try { + return objectMapper.readValue(version.getSnapshotData(), ApiDefinitionSaveReqVO.class); + } catch (JsonProcessingException ex) { + log.error("[API-VERSION] Failed to deserialize snapshot, versionId={}", version.getId(), ex); + throw ServiceExceptionUtil.exception(API_VERSION_SNAPSHOT_DESERIALIZE_FAILED); + } + } + + private boolean snapshotEquals(String left, String right) { + if (Objects.equals(left, right)) { + return true; + } + if (left == null || right == null) { + return false; + } + try { + JsonNode leftNode = objectMapper.readTree(left); + JsonNode rightNode = objectMapper.readTree(right); + return leftNode.equals(rightNode); + } catch (JsonProcessingException ex) { + log.warn("[API-VERSION] Snapshot comparison failed, treat as different", ex); + return false; + } + } + + private JsonNode readTree(String content) { + try { + return objectMapper.readTree(content); + } catch (JsonProcessingException ex) { + log.error("[API-VERSION] Failed to parse snapshot content", ex); + throw ServiceExceptionUtil.exception(API_VERSION_SNAPSHOT_DESERIALIZE_FAILED); + } + } + + private void collectDifferences(JsonNode source, JsonNode target, String path, List differences) { + if (Objects.equals(source, target)) { + return; + } + + if (source == null || source.isNull()) { + addDifference(path, null, target, differences); + return; + } + if (target == null || target.isNull()) { + addDifference(path, source, null, differences); + return; + } + + if (source.isValueNode() && target.isValueNode()) { + if (!Objects.equals(source, target)) { + addDifference(path, source, target, differences); + } + return; + } + + if (source.isObject() && target.isObject()) { + Set fieldNames = new LinkedHashSet<>(); + source.fieldNames().forEachRemaining(fieldNames::add); + target.fieldNames().forEachRemaining(fieldNames::add); + for (String name : fieldNames) { + String childPath = path + "/" + name; + collectDifferences(source.get(name), target.get(name), childPath, differences); + } + return; + } + + if (source.isArray() && target.isArray()) { + int max = Math.max(source.size(), target.size()); + for (int i = 0; i < max; i++) { + String childPath = path + "[" + i + "]"; + JsonNode leftNode = i < source.size() ? source.get(i) : null; + JsonNode rightNode = i < target.size() ? target.get(i) : null; + collectDifferences(leftNode, rightNode, childPath, differences); + } + return; + } + + addDifference(path, source, target, differences); + } + + private void addDifference(String path, JsonNode source, JsonNode target, List differences) { + ApiVersionCompareRespVO.FieldDiff diff = new ApiVersionCompareRespVO.FieldDiff(); + diff.setPath(StringUtils.hasText(path) ? path : "/"); + diff.setSourceValue(source == null || source.isNull() ? "null" : source.toString()); + diff.setTargetValue(target == null || target.isNull() ? "null" : target.toString()); + differences.add(diff); + } + + private String defaultDescription(int versionNumber) { + return String.format("自动版本 v%d", versionNumber); + } + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/GatewayServiceErrorCodeConstants.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/GatewayServiceErrorCodeConstants.java index a82a4a0e..08ee01c5 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/GatewayServiceErrorCodeConstants.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/GatewayServiceErrorCodeConstants.java @@ -55,5 +55,11 @@ public interface GatewayServiceErrorCodeConstants { ErrorCode API_STEP_MAPPING_CONFIG_INVALID = new ErrorCode(1_010_000_046, "步骤映射配置 JSON 非法"); ErrorCode API_CREDENTIAL_ANONYMOUS_USER_REQUIRED = new ErrorCode(1_010_000_047, "启用匿名访问时必须指定固定用户"); ErrorCode API_CREDENTIAL_ANONYMOUS_USER_INVALID = new ErrorCode(1_010_000_048, "匿名访问固定用户不存在或已被禁用"); + ErrorCode API_VERSION_NOT_FOUND = new ErrorCode(1_010_000_049, "API 版本不存在"); + ErrorCode API_VERSION_DUPLICATE = new ErrorCode(1_010_000_050, "API 版本号已存在"); + ErrorCode API_VERSION_SNAPSHOT_SERIALIZE_FAILED = new ErrorCode(1_010_000_051, "API 版本快照序列化失败"); + ErrorCode API_VERSION_SNAPSHOT_DESERIALIZE_FAILED = new ErrorCode(1_010_000_052, "API 版本快照反序列化失败"); + ErrorCode API_VERSION_ACTIVE_CANNOT_DELETE = new ErrorCode(1_010_000_053, "当前激活版本不允许删除"); + ErrorCode API_VERSION_API_MISMATCH = new ErrorCode(1_010_000_054, "两个版本不属于同一 API"); } diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java index aeca8692..6189626d 100644 --- a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java @@ -28,14 +28,15 @@ import java.util.UUID; public final class DatabusApiInvocationExample { public static final String TIMESTAMP = Long.toString(System.currentTimeMillis()); - private static final String APP_ID = "ztmy"; - private static final String APP_SECRET = "zFre/nTRGi7LpoFjN7oQkKeOT09x1fWTyIswrc702QQ="; +// private static final String APP_ID = "ztmy"; +// private static final String APP_SECRET = "zFre/nTRGi7LpoFjN7oQkKeOT09x1fWTyIswrc702QQ="; // private static final String APP_ID = "test"; // private static final String APP_SECRET = "RSYtKXrXPLMy3oeh0cOro6QCioRUgqfnKCkDkNq78sI="; -// private static final String APP_ID = "testAnnoy"; -// private static final String APP_SECRET = "jyGCymUjCFL2i3a4Tm3qBIkUrUl4ZgKPYvOU/47ZWcM="; + private static final String APP_ID = "testAnnoy"; + private static final String APP_SECRET = "jyGCymUjCFL2i3a4Tm3qBIkUrUl4ZgKPYvOU/47ZWcM="; private static final String ENCRYPTION_TYPE = CryptoSignatureUtils.ENCRYPT_TYPE_AES; - private static final String TARGET_API = "http://172.16.46.63:30081/admin-api/databus/api/portal/lgstOpenApi/v1"; +// private static final String TARGET_API = "http://172.16.46.63:30081/admin-api/databus/api/portal/lgstOpenApi/v1"; + private static final String TARGET_API = "http://127.0.0.1:48080/admin-api/databus/api/portal/test/1"; // private static final String TARGET_API = "http://127.0.0.1:48080/admin-api/databus/api/portal/lgstOpenApi/v1"; private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/PermissionController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/PermissionController.java index b77c7f34..f34ec982 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/PermissionController.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/PermissionController.java @@ -1,12 +1,23 @@ package com.zt.plat.module.system.controller.admin.permission; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.zt.plat.framework.common.enums.CommonStatusEnum; import com.zt.plat.framework.common.pojo.CommonResult; +import com.zt.plat.framework.common.util.collection.CollectionUtils; import com.zt.plat.module.system.controller.admin.permission.vo.permission.PermissionAssignRoleDataScopeReqVO; import com.zt.plat.module.system.controller.admin.permission.vo.permission.PermissionAssignRoleMenuReqVO; import com.zt.plat.module.system.controller.admin.permission.vo.permission.PermissionAssignUserRoleReqVO; +import com.zt.plat.module.system.controller.admin.permission.vo.permission.PermissionUserSupervisionRespVO; +import com.zt.plat.module.system.dal.dataobject.permission.MenuDO; +import com.zt.plat.module.system.dal.dataobject.permission.RoleDO; +import com.zt.plat.module.system.dal.dataobject.user.AdminUserDO; +import com.zt.plat.module.system.enums.permission.MenuTypeEnum; +import com.zt.plat.module.system.service.permission.MenuService; import com.zt.plat.module.system.service.permission.PermissionService; +import com.zt.plat.module.system.service.permission.RoleService; import com.zt.plat.module.system.service.tenant.TenantService; +import com.zt.plat.module.system.service.user.AdminUserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -16,7 +27,8 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import java.util.Set; +import java.util.*; +import java.util.stream.Collectors; import static com.zt.plat.framework.common.pojo.CommonResult.success; @@ -34,6 +46,12 @@ public class PermissionController { private PermissionService permissionService; @Resource private TenantService tenantService; + @Resource + private AdminUserService userService; + @Resource + private RoleService roleService; + @Resource + private MenuService menuService; @Operation(summary = "获得角色拥有的菜单编号") @Parameter(name = "roleId", description = "角色编号", required = true) @@ -79,4 +97,285 @@ public class PermissionController { return success(true); } + @Operation(summary = "获得用户权限监督图") + @GetMapping("/user-permission-supervision") + @PreAuthorize("@ss.hasPermission('system:permission:user-permission-supervision')") + public CommonResult getUserPermissionSupervision(@RequestParam("userId") Long userId) { + AdminUserDO user = userService.getUser(userId); + if (user == null) { + return success(null); + } + + PermissionUserSupervisionRespVO respVO = new PermissionUserSupervisionRespVO(); + PermissionUserSupervisionRespVO.Node root = new PermissionUserSupervisionRespVO.Node(); + root.setId("user-" + user.getId()); + root.setLabel(Optional.ofNullable(user.getNickname()).map(nickname -> nickname + "(" + user.getUsername() + ")").orElse(user.getUsername())); + root.setType("user"); + root.setStatus(user.getStatus()); + respVO.setRoot(root); + + Set assignedRoleIds = permissionService.getUserRoleIdListByUserId(userId); + if (CollUtil.isEmpty(assignedRoleIds)) { + root.setTotalPermissionCount(0); + root.setIncrementalPermissionCount(0); + return success(respVO); + } + + Set allRoleIds = roleService.getAllParentAndSelfRoleIds(assignedRoleIds); + List roles = roleService.getRoleList(allRoleIds); + if (CollUtil.isEmpty(roles)) { + root.setTotalPermissionCount(0); + root.setIncrementalPermissionCount(0); + return success(respVO); + } + + Map roleMap = CollectionUtils.convertMap(roles, RoleDO::getId); + List rootRoles = roles.stream() + .filter(role -> role.getParentId() == null || role.getParentId() <= 0 || !roleMap.containsKey(role.getParentId())) + .sorted(getRoleComparator()) + .collect(Collectors.toList()); + + Map> childrenMap = new HashMap<>(); + for (RoleDO role : roles) { + Long parentId = role.getParentId(); + if (parentId != null && parentId > 0 && roleMap.containsKey(parentId)) { + childrenMap.computeIfAbsent(parentId, id -> new ArrayList<>()).add(role); + } + } + childrenMap.values().forEach(list -> list.sort(getRoleComparator())); + + Map> roleMenuMap = new HashMap<>(); + Set aggregatedMenuIds = new LinkedHashSet<>(); + for (RoleDO role : roles) { + Set menuIds = new LinkedHashSet<>(permissionService.getRoleMenuListByRoleId(Collections.singleton(role.getId()))); + roleMenuMap.put(role.getId(), menuIds); + aggregatedMenuIds.addAll(menuIds); + } + + List menuList = menuService.getMenuList(aggregatedMenuIds); + menuList.removeIf(menu -> !CommonStatusEnum.ENABLE.getStatus().equals(menu.getStatus())); + Map menuMap = CollectionUtils.convertMap(menuList, MenuDO::getId); + + List roleNodes = rootRoles.stream() + .map(role -> buildRoleNode(role, childrenMap, roleMenuMap, menuMap, assignedRoleIds, new LinkedHashSet<>())) + .collect(Collectors.toList()); + root.setChildren(roleNodes); + + Set aggregatedEffectiveIds = aggregatedMenuIds.stream() + .map(menuMap::get) + .filter(Objects::nonNull) + .filter(menu -> StrUtil.isNotEmpty(menu.getPermission())) + .map(MenuDO::getId) + .collect(Collectors.toCollection(LinkedHashSet::new)); + root.setTotalPermissionCount(aggregatedEffectiveIds.size()); + root.setIncrementalPermissionCount(aggregatedEffectiveIds.size()); + return success(respVO); + } + + private PermissionUserSupervisionRespVO.Node buildRoleNode(RoleDO role, + Map> childrenMap, + Map> roleMenuMap, + Map menuMap, + Set assignedRoleIds, + Set inheritedMenuIds) { + PermissionUserSupervisionRespVO.Node node = new PermissionUserSupervisionRespVO.Node(); + node.setId("role-" + role.getId()); + node.setLabel(role.getName()); + node.setType("role"); + node.setStatus(role.getStatus()); + node.setCode(role.getCode()); + node.setAssigned(assignedRoleIds.contains(role.getId())); + + Set roleMenuIds = new LinkedHashSet<>(roleMenuMap.getOrDefault(role.getId(), Collections.emptySet())); + Set effectiveRoleMenuIds = roleMenuIds.stream() + .map(menuMap::get) + .filter(Objects::nonNull) + .filter(menu -> StrUtil.isNotEmpty(menu.getPermission())) + .map(MenuDO::getId) + .collect(Collectors.toCollection(LinkedHashSet::new)); + node.setTotalPermissionCount(effectiveRoleMenuIds.size()); + + Set incrementalMenuIds = new LinkedHashSet<>(effectiveRoleMenuIds); + incrementalMenuIds.removeAll(inheritedMenuIds); + node.setIncrementalPermissionCount(incrementalMenuIds.size()); + + Set nextInheritedMenuIds = new LinkedHashSet<>(inheritedMenuIds); + nextInheritedMenuIds.addAll(effectiveRoleMenuIds); + + List childRoles = childrenMap.getOrDefault(role.getId(), Collections.emptyList()); + for (RoleDO childRole : childRoles) { + node.getChildren().add(buildRoleNode(childRole, childrenMap, roleMenuMap, menuMap, assignedRoleIds, + new LinkedHashSet<>(nextInheritedMenuIds))); + } + + List orphanButtons = new ArrayList<>(); + List menuNodes = buildMenuChildren(role, incrementalMenuIds, menuMap, orphanButtons); + node.getChildren().addAll(menuNodes); + + if (!orphanButtons.isEmpty()) { + orphanButtons.stream() + .sorted(getMenuComparator()) + .map(button -> buildPermissionNode(role, button)) + .forEach(node.getChildren()::add); + } + + return node; + } + + private Comparator getRoleComparator() { + return Comparator + .comparing((RoleDO role) -> role.getSort() == null ? Integer.MAX_VALUE : role.getSort()) + .thenComparing(RoleDO::getId); + } + + private Comparator getMenuComparator() { + return Comparator + .comparing((MenuDO menu) -> menu.getSort() == null ? Integer.MAX_VALUE : menu.getSort()) + .thenComparing(MenuDO::getId); + } + + private List buildMenuChildren(RoleDO role, + Set incrementalButtonIds, + Map menuMap, + List orphanButtons) { + if (CollUtil.isEmpty(incrementalButtonIds)) { + return Collections.emptyList(); + } + + Map menuNodeCache = new HashMap<>(); + Map rootMenuNodes = new LinkedHashMap<>(); + + for (Long buttonId : incrementalButtonIds) { + MenuDO button = menuMap.get(buttonId); + if (button == null) { + continue; + } + MenuDO parentMenu = findNearestNonButtonMenu(button, menuMap); + if (parentMenu == null) { + orphanButtons.add(button); + continue; + } + MenuTreeNode parentNode = ensureMenuPath(parentMenu, menuMap, menuNodeCache, rootMenuNodes); + if (parentNode == null) { + orphanButtons.add(button); + continue; + } + parentNode.getButtons().add(button); + } + + return rootMenuNodes.values().stream() + .sorted((left, right) -> getMenuComparator().compare(left.getMenu(), right.getMenu())) + .map(menuTreeNode -> convertMenuTreeNode(role, menuTreeNode)) + .collect(Collectors.toList()); + } + + private MenuTreeNode ensureMenuPath(MenuDO menu, + Map menuMap, + Map menuNodeCache, + Map rootMenuNodes) { + if (menu == null || MenuTypeEnum.BUTTON.getType().equals(menu.getType())) { + return null; + } + MenuTreeNode existing = menuNodeCache.get(menu.getId()); + if (existing != null) { + return existing; + } + + MenuTreeNode current = new MenuTreeNode(menu); + menuNodeCache.put(menu.getId(), current); + + Long parentId = menu.getParentId(); + if (parentId == null || parentId <= 0) { + rootMenuNodes.putIfAbsent(menu.getId(), current); + return current; + } + + MenuDO parentMenu = menuMap.get(parentId); + MenuTreeNode parentNode = ensureMenuPath(parentMenu, menuMap, menuNodeCache, rootMenuNodes); + if (parentNode != null) { + parentNode.getChildren().putIfAbsent(menu.getId(), current); + } else { + rootMenuNodes.putIfAbsent(menu.getId(), current); + } + return current; + } + + private MenuDO findNearestNonButtonMenu(MenuDO menu, Map menuMap) { + MenuDO current = menu; + while (current != null && MenuTypeEnum.BUTTON.getType().equals(current.getType())) { + current = menuMap.get(current.getParentId()); + } + return current; + } + + private PermissionUserSupervisionRespVO.Node convertMenuTreeNode(RoleDO role, MenuTreeNode treeNode) { + PermissionUserSupervisionRespVO.Node menuNode = new PermissionUserSupervisionRespVO.Node(); + MenuDO menu = treeNode.getMenu(); + menuNode.setId("menu-" + role.getId() + '-' + menu.getId()); + menuNode.setLabel(menu.getName()); + menuNode.setType("menu"); + menuNode.setStatus(menu.getStatus()); + + List childMenuNodes = treeNode.getChildren().values().stream() + .sorted((left, right) -> getMenuComparator().compare(left.getMenu(), right.getMenu())) + .map(child -> convertMenuTreeNode(role, child)) + .collect(Collectors.toList()); + menuNode.getChildren().addAll(childMenuNodes); + + List buttonNodes = treeNode.getButtons().stream() + .sorted(getMenuComparator()) + .map(button -> buildPermissionNode(role, button)) + .collect(Collectors.toList()); + menuNode.getButtonChildren().addAll(buttonNodes); + + int totalButtonCount = treeNode.getTotalButtonCount(); + menuNode.setTotalPermissionCount(totalButtonCount); + menuNode.setIncrementalPermissionCount(totalButtonCount); + + return menuNode; + } + + private PermissionUserSupervisionRespVO.Node buildPermissionNode(RoleDO role, MenuDO button) { + PermissionUserSupervisionRespVO.Node permissionNode = new PermissionUserSupervisionRespVO.Node(); + permissionNode.setId("permission-" + role.getId() + '-' + button.getId()); + permissionNode.setLabel(button.getName()); + permissionNode.setType("permission"); + permissionNode.setStatus(button.getStatus()); + permissionNode.setPermission(button.getPermission()); + permissionNode.setTotalPermissionCount(1); + permissionNode.setIncrementalPermissionCount(1); + return permissionNode; + } + + private static final class MenuTreeNode { + + private final MenuDO menu; + private final LinkedHashMap children = new LinkedHashMap<>(); + private final List buttons = new ArrayList<>(); + + private MenuTreeNode(MenuDO menu) { + this.menu = menu; + } + + private MenuDO getMenu() { + return menu; + } + + private LinkedHashMap getChildren() { + return children; + } + + private List getButtons() { + return buttons; + } + + private int getTotalButtonCount() { + int total = buttons.size(); + for (MenuTreeNode child : children.values()) { + total += child.getTotalButtonCount(); + } + return total; + } + } + } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/permission/PermissionUserSupervisionRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/permission/PermissionUserSupervisionRespVO.java new file mode 100644 index 00000000..74e25905 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/permission/PermissionUserSupervisionRespVO.java @@ -0,0 +1,41 @@ +package com.zt.plat.module.system.controller.admin.permission.vo.permission; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 用户权限监督图响应 VO + */ +@Data +public class PermissionUserSupervisionRespVO { + + private Node root; + + @Data + public static class Node { + + private String id; + + private String label; + + private String type; + + private Integer status; + + private Boolean assigned; + + private String code; + + private String permission; + + private Integer totalPermissionCount; + + private Integer incrementalPermissionCount; + + private List children = new ArrayList<>(); + + private List buttonChildren = new ArrayList<>(); + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserPageReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserPageReqVO.java index 48db1230..25dba90b 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserPageReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserPageReqVO.java @@ -9,6 +9,7 @@ import lombok.NoArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; +import java.util.List; import static com.zt.plat.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @@ -38,4 +39,7 @@ public class UserPageReqVO extends PageParam { @Schema(description = "角色编号", example = "1024") private Long roleId; + @Schema(description = "用户编号集合", example = "[1, 2, 3]") + private List ids; + } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/MenuServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/MenuServiceImpl.java index ba10bafe..cb3c136e 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/MenuServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/MenuServiceImpl.java @@ -5,6 +5,7 @@ import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.zt.plat.framework.common.enums.CommonStatusEnum; import com.zt.plat.framework.common.util.object.BeanUtils; import com.zt.plat.module.system.controller.admin.permission.vo.menu.MenuListReqVO; @@ -24,6 +25,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.*; + import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception; import static com.zt.plat.framework.common.util.collection.CollectionUtils.convertList; import static com.zt.plat.framework.common.util.collection.CollectionUtils.convertMap; @@ -66,11 +68,13 @@ public class MenuServiceImpl implements MenuService { } @Override + @Transactional(rollbackFor = Exception.class) @CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, allEntries = true) // allEntries 清空所有缓存,因为 permission 如果变更,涉及到新老两个 permission。直接清理,简单有效 public void updateMenu(MenuSaveVO updateReqVO) { // 校验更新的菜单是否存在 - if (menuMapper.selectById(updateReqVO.getId()) == null) { + MenuDO oldMenu = menuMapper.selectById(updateReqVO.getId()); + if (oldMenu == null) { throw exception(MENU_NOT_EXISTS); } // 校验父菜单存在 @@ -83,6 +87,12 @@ public class MenuServiceImpl implements MenuService { MenuDO updateObj = BeanUtils.toBean(updateReqVO, MenuDO.class); initMenuProperty(updateObj); menuMapper.updateById(updateObj); + + // 如果本次更新禁用了当前菜单,则联动禁用所有子菜单 + if (!CommonStatusEnum.isDisable(oldMenu.getStatus()) + && CommonStatusEnum.isDisable(updateObj.getStatus())) { + cascadeDisableChildMenus(updateObj.getId()); + } } @Override @@ -190,6 +200,41 @@ public class MenuServiceImpl implements MenuService { return menuMapper.selectBatchIds(ids); } + // 禁用父级菜单时需级联禁用所有子菜单 + private void cascadeDisableChildMenus(Long parentId) { + List allMenus = menuMapper.selectList(); + if (CollUtil.isEmpty(allMenus)) { + return; + } + Map> childrenMap = new HashMap<>(); + for (MenuDO menu : allMenus) { + childrenMap.computeIfAbsent(menu.getParentId(), key -> new ArrayList<>()).add(menu); + } + + Deque queue = new ArrayDeque<>(); + queue.add(parentId); + List toDisableIds = new ArrayList<>(); + while (!queue.isEmpty()) { + Long current = queue.poll(); + List children = childrenMap.get(current); + if (CollUtil.isEmpty(children)) { + continue; + } + for (MenuDO child : children) { + if (!CommonStatusEnum.isDisable(child.getStatus())) { + toDisableIds.add(child.getId()); + } + queue.add(child.getId()); + } + } + if (CollUtil.isEmpty(toDisableIds)) { + return; + } + menuMapper.update(null, new LambdaUpdateWrapper() + .in(MenuDO::getId, toDisableIds) + .set(MenuDO::getStatus, CommonStatusEnum.DISABLE.getStatus())); + } + /** * 校验父菜单是否合法 *

diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserServiceImpl.java index 0df5d241..d4ef744b 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserServiceImpl.java @@ -338,6 +338,18 @@ public class AdminUserServiceImpl implements AdminUserService { Set userIds = reqVO.getRoleId() != null ? permissionService.getUserRoleIdListByRoleId(singleton(reqVO.getRoleId())) : null; + if (CollUtil.isNotEmpty(reqVO.getIds())) { + Set idFilter = new HashSet<>(reqVO.getIds()); + if (userIds == null) { + userIds = idFilter; + } else { + userIds.retainAll(idFilter); + } + if (CollUtil.isEmpty(userIds)) { + return PageResult.empty(); + } + } + // 分页查询 PageResult pageResult = userMapper.selectPage(reqVO, getDeptCondition(reqVO.getDeptId()), userIds); fillUserDeptInfo(pageResult.getList());