1.规范增量 SQL 文件命名

2.新增数据总线模块(未完成)
3.新增规则模块(未完成)
4.新增组织编码与外部系统组织编码映射关系表
5.补全 e 办单点登录回调逻辑
This commit is contained in:
chenbowen
2025-10-15 08:59:57 +08:00
parent 97bd87de55
commit c0dc0823b6
246 changed files with 11118 additions and 2749 deletions

View File

@@ -0,0 +1,31 @@
-- 清理旧数据,确保脚本可重复执行
DELETE FROM system_menu WHERE id IN (6500,6501,650101,650102,650103);
-- 顶级目录(父级假定为 id=2 的系统管理目录)
INSERT INTO system_menu (
id, name, permission, type, sort, parent_id,
path, icon, component, status, component_name
) VALUES (
6500, '统一外部网关', '', 1, 20, 1,
'databus', 'ep:data-line', '', 0, NULL
);
-- API 门户页面
INSERT INTO system_menu (
id, name, permission, type, sort, parent_id,
path, icon, component, status, component_name
) VALUES (
6501, 'API 门户', 'databus:gateway:query', 2, 1, 6500,
'databus-gateway', 'ep:cpu', 'databus/gateway/index', 0, 'DatabusGateway'
);
-- 页面内操作按钮权限
INSERT INTO system_menu (
id, name, permission, type, sort, parent_id,
path, icon, component, status
) VALUES
(650101, 'API 列表', 'databus:gateway:query', 3, 1, 6501, '', '', '', 0),
(650102, 'API 调试', 'databus:gateway:invoke', 3, 2, 6501, '', '', '', 0),
(650103, '刷新定义', 'databus:gateway:refresh', 3, 3, 6501, '', '', '', 0);
d

View File

@@ -0,0 +1,241 @@
/*
* Databus API portal schema for DM8
* Generated on 2025-10-10
*/
-- ----------------------------
-- Table structure for databus_api_definition
-- ----------------------------
CREATE TABLE databus_api_definition (
id BIGINT NOT NULL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
api_code VARCHAR(128) NOT NULL,
uri_pattern VARCHAR(256) NOT NULL,
http_method VARCHAR(16) NOT NULL,
version VARCHAR(32) NOT NULL,
status SMALLINT DEFAULT 0 NOT NULL,
description VARCHAR(512),
auth_policy_id BIGINT,
rate_limit_id BIGINT,
response_template CLOB,
cache_strategy VARCHAR(128),
updated_at DATETIME,
grey_released BIT DEFAULT '0' NOT NULL,
creator VARCHAR(64) DEFAULT '' NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updater VARCHAR(64) DEFAULT '' NOT NULL,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted BIT DEFAULT '0' NOT NULL
);
CREATE UNIQUE INDEX uk_databus_api_definition_code_ver ON databus_api_definition (tenant_id, api_code, version);
CREATE INDEX idx_databus_api_definition_status ON databus_api_definition (tenant_id, status);
CREATE INDEX idx_databus_api_definition_policy ON databus_api_definition (tenant_id, auth_policy_id, rate_limit_id);
COMMENT ON TABLE databus_api_definition IS '统一外部 API 门户 - API 定义表';
COMMENT ON COLUMN databus_api_definition.id IS '主键 ID';
COMMENT ON COLUMN databus_api_definition.tenant_id IS '租户编号';
COMMENT ON COLUMN databus_api_definition.api_code IS 'API 编码';
COMMENT ON COLUMN databus_api_definition.uri_pattern IS '匹配路径模板';
COMMENT ON COLUMN databus_api_definition.http_method IS 'HTTP 方法';
COMMENT ON COLUMN databus_api_definition.version IS '版本号';
COMMENT ON COLUMN databus_api_definition.status IS '发布状态';
COMMENT ON COLUMN databus_api_definition.description IS '描述信息';
COMMENT ON COLUMN databus_api_definition.auth_policy_id IS '认证策略 ID';
COMMENT ON COLUMN databus_api_definition.rate_limit_id IS '限流策略 ID';
COMMENT ON COLUMN databus_api_definition.response_template IS '响应模板 JSON';
COMMENT ON COLUMN databus_api_definition.cache_strategy IS '缓存策略配置';
COMMENT ON COLUMN databus_api_definition.updated_at IS '业务更新时间';
COMMENT ON COLUMN databus_api_definition.grey_released IS '灰度发布标记';
COMMENT ON COLUMN databus_api_definition.creator IS '创建者';
COMMENT ON COLUMN databus_api_definition.create_time IS '创建时间';
COMMENT ON COLUMN databus_api_definition.updater IS '更新者';
COMMENT ON COLUMN databus_api_definition.update_time IS '更新时间';
COMMENT ON COLUMN databus_api_definition.deleted IS '逻辑删除标记';
-- ----------------------------
-- Table structure for databus_api_flow_publish
-- ----------------------------
CREATE TABLE databus_api_flow_publish (
id BIGINT NOT NULL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
api_id BIGINT NOT NULL,
release_tag VARCHAR(64),
snapshot CLOB,
status VARCHAR(32),
active BIT DEFAULT '0' NOT NULL,
description VARCHAR(512),
creator VARCHAR(64) DEFAULT '' NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updater VARCHAR(64) DEFAULT '' NOT NULL,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted BIT DEFAULT '0' NOT NULL
);
CREATE INDEX idx_databus_api_flow_publish_api ON databus_api_flow_publish (tenant_id, api_id, active);
COMMENT ON TABLE databus_api_flow_publish IS '统一外部 API 门户 - 发布记录表';
COMMENT ON COLUMN databus_api_flow_publish.id IS '主键 ID';
COMMENT ON COLUMN databus_api_flow_publish.tenant_id IS '租户编号';
COMMENT ON COLUMN databus_api_flow_publish.api_id IS '关联的 API ID';
COMMENT ON COLUMN databus_api_flow_publish.release_tag IS '发布批次标识';
COMMENT ON COLUMN databus_api_flow_publish.snapshot IS '配置快照 JSON';
COMMENT ON COLUMN databus_api_flow_publish.status IS '发布状态';
COMMENT ON COLUMN databus_api_flow_publish.active IS '是否当前生效';
COMMENT ON COLUMN databus_api_flow_publish.description IS '备注信息';
COMMENT ON COLUMN databus_api_flow_publish.creator IS '创建者';
COMMENT ON COLUMN databus_api_flow_publish.create_time IS '创建时间';
COMMENT ON COLUMN databus_api_flow_publish.updater IS '更新者';
COMMENT ON COLUMN databus_api_flow_publish.update_time IS '更新时间';
COMMENT ON COLUMN databus_api_flow_publish.deleted IS '逻辑删除标记';
-- ----------------------------
-- Table structure for databus_policy_auth
-- ----------------------------
CREATE TABLE databus_policy_auth (
id BIGINT NOT NULL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
name VARCHAR(128) NOT NULL,
type VARCHAR(64) NOT NULL,
config CLOB,
description VARCHAR(512),
creator VARCHAR(64) DEFAULT '' NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updater VARCHAR(64) DEFAULT '' NOT NULL,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted BIT DEFAULT '0' NOT NULL
);
CREATE UNIQUE INDEX uk_databus_policy_auth_name ON databus_policy_auth (tenant_id, name);
COMMENT ON TABLE databus_policy_auth IS '统一外部 API 门户 - 认证策略表';
COMMENT ON COLUMN databus_policy_auth.id IS '主键 ID';
COMMENT ON COLUMN databus_policy_auth.tenant_id IS '租户编号';
COMMENT ON COLUMN databus_policy_auth.name IS '策略名称';
COMMENT ON COLUMN databus_policy_auth.type IS '策略类型';
COMMENT ON COLUMN databus_policy_auth.config IS '策略配置 JSON';
COMMENT ON COLUMN databus_policy_auth.description IS '描述信息';
COMMENT ON COLUMN databus_policy_auth.creator IS '创建者';
COMMENT ON COLUMN databus_policy_auth.create_time IS '创建时间';
COMMENT ON COLUMN databus_policy_auth.updater IS '更新者';
COMMENT ON COLUMN databus_policy_auth.update_time IS '更新时间';
COMMENT ON COLUMN databus_policy_auth.deleted IS '逻辑删除标记';
-- ----------------------------
-- Table structure for databus_policy_rate_limit
-- ----------------------------
CREATE TABLE databus_policy_rate_limit (
id BIGINT NOT NULL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
name VARCHAR(128) NOT NULL,
type VARCHAR(64) NOT NULL,
config CLOB,
description VARCHAR(512),
creator VARCHAR(64) DEFAULT '' NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updater VARCHAR(64) DEFAULT '' NOT NULL,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted BIT DEFAULT '0' NOT NULL
);
CREATE UNIQUE INDEX uk_databus_policy_rate_limit_name ON databus_policy_rate_limit (tenant_id, name);
COMMENT ON TABLE databus_policy_rate_limit IS '统一外部 API 门户 - 限流策略表';
COMMENT ON COLUMN databus_policy_rate_limit.id IS '主键 ID';
COMMENT ON COLUMN databus_policy_rate_limit.tenant_id IS '租户编号';
COMMENT ON COLUMN databus_policy_rate_limit.name IS '策略名称';
COMMENT ON COLUMN databus_policy_rate_limit.type IS '策略类型';
COMMENT ON COLUMN databus_policy_rate_limit.config IS '策略配置 JSON';
COMMENT ON COLUMN databus_policy_rate_limit.description IS '描述信息';
COMMENT ON COLUMN databus_policy_rate_limit.creator IS '创建者';
COMMENT ON COLUMN databus_policy_rate_limit.create_time IS '创建时间';
COMMENT ON COLUMN databus_policy_rate_limit.updater IS '更新者';
COMMENT ON COLUMN databus_policy_rate_limit.update_time IS '更新时间';
COMMENT ON COLUMN databus_policy_rate_limit.deleted IS '逻辑删除标记';
-- ----------------------------
-- Table structure for databus_api_step
-- ----------------------------
CREATE TABLE databus_api_step (
id BIGINT NOT NULL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
api_id BIGINT NOT NULL,
step_order INT DEFAULT 0 NOT NULL,
parallel_group VARCHAR(64),
type VARCHAR(32) NOT NULL,
target_endpoint VARCHAR(512),
request_mapping_expr CLOB,
response_mapping_expr CLOB,
transform_id BIGINT,
timeout BIGINT,
retry_strategy CLOB,
fallback_strategy CLOB,
condition_expr CLOB,
stop_on_error BIT DEFAULT '0' NOT NULL,
creator VARCHAR(64) DEFAULT '' NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updater VARCHAR(64) DEFAULT '' NOT NULL,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted BIT DEFAULT '0' NOT NULL
);
CREATE INDEX idx_databus_api_step_api_order ON databus_api_step (tenant_id, api_id, parallel_group, step_order);
COMMENT ON TABLE databus_api_step IS '统一外部 API 门户 - 编排步骤表';
COMMENT ON COLUMN databus_api_step.id IS '主键 ID';
COMMENT ON COLUMN databus_api_step.tenant_id IS '租户编号';
COMMENT ON COLUMN databus_api_step.api_id IS '关联的 API ID';
COMMENT ON COLUMN databus_api_step.step_order IS '执行顺序';
COMMENT ON COLUMN databus_api_step.parallel_group IS '并行分组标识';
COMMENT ON COLUMN databus_api_step.type IS '步骤类型';
COMMENT ON COLUMN databus_api_step.target_endpoint IS '目标端点';
COMMENT ON COLUMN databus_api_step.request_mapping_expr IS '请求映射表达式';
COMMENT ON COLUMN databus_api_step.response_mapping_expr IS '响应映射表达式';
COMMENT ON COLUMN databus_api_step.transform_id IS '默认变换 ID';
COMMENT ON COLUMN databus_api_step.timeout IS '超时时间(毫秒)';
COMMENT ON COLUMN databus_api_step.retry_strategy IS '重试策略 JSON';
COMMENT ON COLUMN databus_api_step.fallback_strategy IS '降级策略 JSON';
COMMENT ON COLUMN databus_api_step.condition_expr IS '执行条件表达式';
COMMENT ON COLUMN databus_api_step.stop_on_error IS '出错是否终止';
COMMENT ON COLUMN databus_api_step.creator IS '创建者';
COMMENT ON COLUMN databus_api_step.create_time IS '创建时间';
COMMENT ON COLUMN databus_api_step.updater IS '更新者';
COMMENT ON COLUMN databus_api_step.update_time IS '更新时间';
COMMENT ON COLUMN databus_api_step.deleted IS '逻辑删除标记';
-- ----------------------------
-- Table structure for databus_api_transform
-- ----------------------------
CREATE TABLE databus_api_transform (
id BIGINT NOT NULL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
api_id BIGINT,
step_id BIGINT,
phase VARCHAR(32) NOT NULL,
expression_type VARCHAR(32) NOT NULL,
expression CLOB NOT NULL,
description VARCHAR(512),
creator VARCHAR(64) DEFAULT '' NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updater VARCHAR(64) DEFAULT '' NOT NULL,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted BIT DEFAULT '0' NOT NULL
);
CREATE INDEX idx_databus_api_transform_api ON databus_api_transform (tenant_id, api_id);
CREATE INDEX idx_databus_api_transform_step ON databus_api_transform (tenant_id, step_id);
COMMENT ON TABLE databus_api_transform IS '统一外部 API 门户 - 变换配置表';
COMMENT ON COLUMN databus_api_transform.id IS '主键 ID';
COMMENT ON COLUMN databus_api_transform.tenant_id IS '租户编号';
COMMENT ON COLUMN databus_api_transform.api_id IS '关联的 API ID';
COMMENT ON COLUMN databus_api_transform.step_id IS '关联的步骤 ID';
COMMENT ON COLUMN databus_api_transform.phase IS '执行阶段';
COMMENT ON COLUMN databus_api_transform.expression_type IS '表达式类型';
COMMENT ON COLUMN databus_api_transform.expression IS '表达式内容';
COMMENT ON COLUMN databus_api_transform.description IS '描述信息';
COMMENT ON COLUMN databus_api_transform.creator IS '创建者';
COMMENT ON COLUMN databus_api_transform.create_time IS '创建时间';
COMMENT ON COLUMN databus_api_transform.updater IS '更新者';
COMMENT ON COLUMN databus_api_transform.update_time IS '更新时间';
COMMENT ON COLUMN databus_api_transform.deleted IS '逻辑删除标记';

View File

@@ -0,0 +1,228 @@
-- 规则引擎模块核心表结构DM8
-- 如果需要重建表,请在执行前备份现有数据
DROP TABLE IF EXISTS rule_release_record;
DROP TABLE IF EXISTS rule_business_relation;
DROP TABLE IF EXISTS rule_business;
DROP TABLE IF EXISTS rule_chain_dependency;
DROP TABLE IF EXISTS rule_chain;
DROP TABLE IF EXISTS rule_definition;
CREATE TABLE rule_definition (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
code VARCHAR(128) NOT NULL,
name VARCHAR(128) NOT NULL,
type SMALLINT NOT NULL,
dsl CLOB,
script_language VARCHAR(64),
bean_ref VARCHAR(128),
config_json CLOB,
status SMALLINT DEFAULT 0 NOT NULL,
version VARCHAR(64),
remark VARCHAR(512),
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 TINYINT DEFAULT 0 NOT NULL,
tenant_id BIGINT DEFAULT 0 NOT NULL
);
COMMENT ON TABLE rule_definition IS '规则定义表';
COMMENT ON COLUMN rule_definition.id IS '规则定义编号';
COMMENT ON COLUMN rule_definition.code IS '规则编码';
COMMENT ON COLUMN rule_definition.name IS '规则名称';
COMMENT ON COLUMN rule_definition.type IS '规则类型';
COMMENT ON COLUMN rule_definition.dsl IS 'LiteFlow DSL 脚本';
COMMENT ON COLUMN rule_definition.script_language IS '脚本语言';
COMMENT ON COLUMN rule_definition.bean_ref IS 'Spring Bean 引用';
COMMENT ON COLUMN rule_definition.config_json IS '规则配置 JSON';
COMMENT ON COLUMN rule_definition.status IS '规则状态';
COMMENT ON COLUMN rule_definition.version IS '规则版本号';
COMMENT ON COLUMN rule_definition.remark IS '备注';
COMMENT ON COLUMN rule_definition.creator IS '创建者';
COMMENT ON COLUMN rule_definition.create_time IS '创建时间';
COMMENT ON COLUMN rule_definition.updater IS '更新者';
COMMENT ON COLUMN rule_definition.update_time IS '更新时间';
COMMENT ON COLUMN rule_definition.deleted IS '是否删除';
COMMENT ON COLUMN rule_definition.tenant_id IS '租户编号';
CREATE UNIQUE INDEX uk_rule_definition_code_tenant ON rule_definition (code, tenant_id);
CREATE TABLE rule_chain (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
code VARCHAR(128) NOT NULL,
name VARCHAR(128) NOT NULL,
description VARCHAR(512),
structure_json CLOB,
liteflow_dsl CLOB,
status SMALLINT DEFAULT 0 NOT NULL,
version VARCHAR(64),
remark VARCHAR(512),
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 TINYINT DEFAULT 0 NOT NULL,
tenant_id BIGINT DEFAULT 0 NOT NULL
);
COMMENT ON TABLE rule_chain IS '规则链表';
COMMENT ON COLUMN rule_chain.id IS '规则链编号';
COMMENT ON COLUMN rule_chain.code IS '规则链编码';
COMMENT ON COLUMN rule_chain.name IS '规则链名称';
COMMENT ON COLUMN rule_chain.description IS '规则链描述';
COMMENT ON COLUMN rule_chain.structure_json IS '链路结构 JSON';
COMMENT ON COLUMN rule_chain.liteflow_dsl IS 'LiteFlow DSL 内容';
COMMENT ON COLUMN rule_chain.status IS '规则链状态';
COMMENT ON COLUMN rule_chain.version IS '版本号';
COMMENT ON COLUMN rule_chain.remark IS '备注';
COMMENT ON COLUMN rule_chain.creator IS '创建者';
COMMENT ON COLUMN rule_chain.create_time IS '创建时间';
COMMENT ON COLUMN rule_chain.updater IS '更新者';
COMMENT ON COLUMN rule_chain.update_time IS '更新时间';
COMMENT ON COLUMN rule_chain.deleted IS '是否删除';
COMMENT ON COLUMN rule_chain.tenant_id IS '租户编号';
CREATE UNIQUE INDEX uk_rule_chain_code_tenant ON rule_chain (code, tenant_id);
CREATE INDEX idx_rule_chain_status ON rule_chain (status);
CREATE TABLE rule_chain_dependency (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
parent_chain_id BIGINT NOT NULL,
child_rule_id BIGINT NOT NULL,
link_type SMALLINT NOT NULL,
order_index INTEGER,
parallel_group VARCHAR(64),
condition_expr VARCHAR(512),
config_json CLOB,
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 TINYINT DEFAULT 0 NOT NULL,
tenant_id BIGINT DEFAULT 0 NOT NULL
);
COMMENT ON TABLE rule_chain_dependency IS '规则链依赖表';
COMMENT ON COLUMN rule_chain_dependency.id IS '依赖编号';
COMMENT ON COLUMN rule_chain_dependency.parent_chain_id IS '父规则链编号';
COMMENT ON COLUMN rule_chain_dependency.child_rule_id IS '引用的规则定义编号';
COMMENT ON COLUMN rule_chain_dependency.link_type IS '节点类型';
COMMENT ON COLUMN rule_chain_dependency.order_index IS '执行顺序';
COMMENT ON COLUMN rule_chain_dependency.parallel_group IS '并行组标识';
COMMENT ON COLUMN rule_chain_dependency.condition_expr IS '条件表达式';
COMMENT ON COLUMN rule_chain_dependency.config_json IS '节点配置 JSON';
COMMENT ON COLUMN rule_chain_dependency.creator IS '创建者';
COMMENT ON COLUMN rule_chain_dependency.create_time IS '创建时间';
COMMENT ON COLUMN rule_chain_dependency.updater IS '更新者';
COMMENT ON COLUMN rule_chain_dependency.update_time IS '更新时间';
COMMENT ON COLUMN rule_chain_dependency.deleted IS '是否删除';
COMMENT ON COLUMN rule_chain_dependency.tenant_id IS '租户编号';
CREATE INDEX idx_rule_chain_dependency_parent ON rule_chain_dependency (parent_chain_id);
CREATE INDEX idx_rule_chain_dependency_child ON rule_chain_dependency (child_rule_id);
CREATE TABLE rule_business (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
business VARCHAR(128) NOT NULL,
rule_chain_id BIGINT,
override_strategy SMALLINT DEFAULT 0 NOT NULL,
locked TINYINT DEFAULT 0 NOT NULL,
effective_version VARCHAR(64),
config_json CLOB,
remark VARCHAR(512),
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 TINYINT DEFAULT 0 NOT NULL,
tenant_id BIGINT DEFAULT 0 NOT NULL
);
COMMENT ON TABLE rule_business IS '业务规则绑定表';
COMMENT ON COLUMN rule_business.id IS '业务绑定编号';
COMMENT ON COLUMN rule_business.business IS '业务标识';
COMMENT ON COLUMN rule_business.rule_chain_id IS '绑定的规则链编号';
COMMENT ON COLUMN rule_business.override_strategy IS '覆盖策略';
COMMENT ON COLUMN rule_business.locked IS '是否锁定';
COMMENT ON COLUMN rule_business.effective_version IS '生效版本';
COMMENT ON COLUMN rule_business.config_json IS '业务配置 JSON';
COMMENT ON COLUMN rule_business.remark IS '备注';
COMMENT ON COLUMN rule_business.creator IS '创建者';
COMMENT ON COLUMN rule_business.create_time IS '创建时间';
COMMENT ON COLUMN rule_business.updater IS '更新者';
COMMENT ON COLUMN rule_business.update_time IS '更新时间';
COMMENT ON COLUMN rule_business.deleted IS '是否删除';
COMMENT ON COLUMN rule_business.tenant_id IS '租户编号';
CREATE UNIQUE INDEX uk_rule_business_tenant ON rule_business (business, tenant_id);
CREATE INDEX idx_rule_business_chain ON rule_business (rule_chain_id);
CREATE TABLE rule_business_relation (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
parent_business VARCHAR(128) NOT NULL,
child_business VARCHAR(128) NOT NULL,
sort INTEGER DEFAULT 0 NOT NULL,
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 TINYINT DEFAULT 0 NOT NULL,
tenant_id BIGINT DEFAULT 0 NOT NULL
);
COMMENT ON TABLE rule_business_relation IS '业务继承关系表';
COMMENT ON COLUMN rule_business_relation.id IS '继承关系编号';
COMMENT ON COLUMN rule_business_relation.parent_business IS '父业务标识';
COMMENT ON COLUMN rule_business_relation.child_business IS '子业务标识';
COMMENT ON COLUMN rule_business_relation.sort IS '排序';
COMMENT ON COLUMN rule_business_relation.creator IS '创建者';
COMMENT ON COLUMN rule_business_relation.create_time IS '创建时间';
COMMENT ON COLUMN rule_business_relation.updater IS '更新者';
COMMENT ON COLUMN rule_business_relation.update_time IS '更新时间';
COMMENT ON COLUMN rule_business_relation.deleted IS '是否删除';
COMMENT ON COLUMN rule_business_relation.tenant_id IS '租户编号';
CREATE UNIQUE INDEX uk_rule_business_relation_child ON rule_business_relation (child_business, tenant_id);
CREATE INDEX idx_rule_business_relation_parent ON rule_business_relation (parent_business);
CREATE TABLE rule_release_record (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
business VARCHAR(128) NOT NULL,
chain_id VARCHAR(255) NOT NULL,
chain_code VARCHAR(128),
version VARCHAR(64) NOT NULL,
status SMALLINT DEFAULT 0 NOT NULL,
release_user_id BIGINT,
release_user_name VARCHAR(128),
release_time TIMESTAMP,
remark VARCHAR(512),
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 TINYINT DEFAULT 0 NOT NULL,
tenant_id BIGINT DEFAULT 0 NOT NULL
);
COMMENT ON TABLE rule_release_record IS '规则发布记录表';
COMMENT ON COLUMN rule_release_record.id IS '发布记录编号';
COMMENT ON COLUMN rule_release_record.business IS '业务标识';
COMMENT ON COLUMN rule_release_record.chain_id IS '生成链路标识';
COMMENT ON COLUMN rule_release_record.chain_code IS '规则链编码';
COMMENT ON COLUMN rule_release_record.version IS '发布版本';
COMMENT ON COLUMN rule_release_record.status IS '发布状态';
COMMENT ON COLUMN rule_release_record.release_user_id IS '发布人编号';
COMMENT ON COLUMN rule_release_record.release_user_name IS '发布人名称';
COMMENT ON COLUMN rule_release_record.release_time IS '发布时间';
COMMENT ON COLUMN rule_release_record.remark IS '备注';
COMMENT ON COLUMN rule_release_record.creator IS '创建者';
COMMENT ON COLUMN rule_release_record.create_time IS '创建时间';
COMMENT ON COLUMN rule_release_record.updater IS '更新者';
COMMENT ON COLUMN rule_release_record.update_time IS '更新时间';
COMMENT ON COLUMN rule_release_record.deleted IS '是否删除';
COMMENT ON COLUMN rule_release_record.tenant_id IS '租户编号';
CREATE INDEX idx_rule_release_business_time ON rule_release_record (business, release_time);
CREATE UNIQUE INDEX uk_rule_release_business_version ON rule_release_record (business, version, tenant_id);

View File

@@ -0,0 +1,27 @@
-- 规则引擎模块菜单与权限初始化DM8
-- 顶级目录放置在系统管理(2)下,如需调整请修改 parent_id
DELETE FROM system_menu WHERE id IN (6100,6101,610101,610102,610103,610104,610111,610112,610113,610114,610121,610122,610123,610124,610131,610132,610133,610141);
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
(6100, '规则引擎', '', 1, 20, 2, 'rule', 'ep:s-operation', '', 'RuleModule', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'),
(6101, '规则设计器', '', 2, 1, 6100, 'designer', 'ep:s-operation', 'rule/designer/index', 'RuleDesigner', 0, '1', '0', '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
(610101, '规则定义查询', 'rule:definition:query', 3, 1, 6101, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'),
(610102, '规则定义创建', 'rule:definition:create', 3, 2, 6101, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'),
(610103, '规则定义更新', 'rule:definition:update', 3, 3, 6101, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'),
(610104, '规则定义删除', 'rule:definition:delete', 3, 4, 6101, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'),
(610111, '规则链查询', 'rule:chain:query', 3, 5, 6101, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'),
(610112, '规则链创建', 'rule:chain:create', 3, 6, 6101, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'),
(610113, '规则链更新', 'rule:chain:update', 3, 7, 6101, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'),
(610114, '规则链删除', 'rule:chain:delete', 3, 8, 6101, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'),
(610121, '业务链查询', 'rule:business:query', 3, 9, 6101, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'),
(610122, '业务链维护', 'rule:business:update', 3, 10, 6101, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'),
(610123, '业务链删除', 'rule:business:delete', 3, 11, 6101, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'),
(610124, '业务链预览', 'rule:business:preview', 3, 12, 6101, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'),
(610131, '发布记录查询', 'rule:publish:query', 3, 13, 6101, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'),
(610132, '规则链发布', 'rule:publish:publish', 3, 14, 6101, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'),
(610133, '规则链回滚', 'rule:publish:rollback', 3, 15, 6101, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'),
(610141, '规则模拟执行', 'rule:simulation:execute', 3, 16, 6101, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0');

View File

@@ -0,0 +1,60 @@
-- DM8 部门外部组织编码映射初始化脚本
-- 包含表结构、字段注释及基础字典数据
-- 重复执行时请先备份数据
DROP TABLE IF EXISTS system_dept_external_code;
CREATE TABLE system_dept_external_code (
id BIGINT NOT NULL,
dept_id BIGINT NOT NULL,
system_code VARCHAR(64) NOT NULL,
external_dept_code VARCHAR(128) NOT NULL,
external_dept_name VARCHAR(255),
status TINYINT DEFAULT 0 NOT NULL,
remark VARCHAR(512),
tenant_id BIGINT DEFAULT 0,
creator VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted TINYINT DEFAULT 0 NOT NULL,
CONSTRAINT pk_system_dept_external_code PRIMARY KEY (id)
);
-- 唯一索引与辅助索引
CREATE UNIQUE INDEX uk_system_dept_external_code_ext
ON system_dept_external_code (tenant_id, system_code, external_dept_code);
CREATE UNIQUE INDEX uk_system_dept_external_code_dept
ON system_dept_external_code (tenant_id, system_code, dept_id);
CREATE INDEX idx_system_dept_external_code_dept
ON system_dept_external_code (tenant_id, dept_id);
COMMENT ON TABLE system_dept_external_code IS '部门外部组织编码映射';
COMMENT ON COLUMN system_dept_external_code.id IS '主键编号';
COMMENT ON COLUMN system_dept_external_code.dept_id IS '本系统部门编号';
COMMENT ON COLUMN system_dept_external_code.system_code IS '外部系统标识';
COMMENT ON COLUMN system_dept_external_code.external_dept_code IS '外部组织编码';
COMMENT ON COLUMN system_dept_external_code.external_dept_name IS '外部组织名称';
COMMENT ON COLUMN system_dept_external_code.status IS '状态0开启 1关闭';
COMMENT ON COLUMN system_dept_external_code.remark IS '备注';
COMMENT ON COLUMN system_dept_external_code.tenant_id IS '租户编号';
COMMENT ON COLUMN system_dept_external_code.creator IS '创建者';
COMMENT ON COLUMN system_dept_external_code.create_time IS '创建时间';
COMMENT ON COLUMN system_dept_external_code.updater IS '更新者';
COMMENT ON COLUMN system_dept_external_code.update_time IS '更新时间';
COMMENT ON COLUMN system_dept_external_code.deleted IS '删除标记';
-- 初始化外部系统标识字典
INSERT INTO system_dict_type (id, name, type, status, remark, creator, create_time, updater, update_time, deleted)
SELECT 20050, '外部系统标识', 'system_dept_external_system', 0, '部门外部组织编码中的外部系统标识', 'admin', SYSDATE, 'admin', SYSDATE, 0
FROM dual
WHERE NOT EXISTS (
SELECT 1 FROM system_dict_type WHERE type = 'system_dept_external_system'
);
INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted)
SELECT 2005001, 1, 'ERP 系统', 'ERP', 'system_dept_external_system', 0, '', '', '企业资源计划系统', 'admin', SYSDATE, 'admin', SYSDATE, 0
FROM dual
WHERE NOT EXISTS (
SELECT 1 FROM system_dict_data WHERE dict_type = 'system_dept_external_system' AND value = 'ERP'
);

View File

@@ -0,0 +1,35 @@
-- DM8 部门外部组织编码映射菜单与权限脚本
-- 清理旧数据并重新创建目录、页面及操作按钮
-- 保持脚本幂等性,先清理旧数据
DELETE FROM system_role_menu WHERE menu_id IN (6200, 6201, 620101, 620102, 620103, 620104);
DELETE FROM system_menu WHERE id IN (6200, 6201, 620101, 620102, 620103, 620104);
-- 在系统管理ID=2下创建目录与页面
INSERT INTO system_menu (
id, name, permission, type, sort, parent_id,
path, icon, component, status, component_name
) VALUES (
6200, '组织编码映射', '', 1, 25, 2,
'dept-external', 'ep:connection', '', 0, 'DeptExternalCodeRoot'
);
INSERT INTO system_menu (
id, name, permission, type, sort, parent_id,
path, icon, component, status, component_name
) VALUES (
6201, '外部组织编码', '', 2, 1, 6200,
'dept-external-code', 'ep:connection', 'system/deptExternalCode/index', 0, 'SystemDeptExternalCode'
);
-- 创建操作按钮权限
INSERT INTO system_menu (
id, name, permission, type, sort, parent_id,
path, icon, component, status
) VALUES
(620101, '查询部门外部编码', 'system:dept-external-code:query', 3, 1, 6201, '', '', '', 0),
(620102, '新增部门外部编码', 'system:dept-external-code:create', 3, 2, 6201, '', '', '', 0),
(620103, '修改部门外部编码', 'system:dept-external-code:update', 3, 3, 6201, '', '', '', 0),
(620104, '删除部门外部编码', 'system:dept-external-code:delete', 3, 4, 6201, '', '', '', 0);
-- 如需分配给角色,请按本地序列策略写入 system_role_menu

View File

@@ -0,0 +1,65 @@
START TRANSACTION;
-- Cleanup previous sample records by identifier range
DELETE FROM databus_api_transform WHERE id BETWEEN 520100000000000000 AND 520100000000000999;
DELETE FROM databus_api_step WHERE id BETWEEN 610100000000000000 AND 610100000000000999;
DELETE FROM databus_api_definition WHERE id BETWEEN 410100000000000000 AND 410100000000000999;
DELETE FROM databus_policy_auth WHERE id BETWEEN 110100000000000000 AND 110100000000000999;
DELETE FROM databus_policy_rate_limit WHERE id BETWEEN 210100000000000000 AND 210100000000000999;
DELETE FROM databus_api_flow_publish WHERE id BETWEEN 710100000000000000 AND 710100000000000999;
-- Authentication policies aligned with DefaultAuthPolicyEvaluator header strategy
INSERT INTO databus_policy_auth
(id, tenant_id, name, type, config, description, creator, create_time, updater, update_time, deleted)
VALUES
(110100000000000001, 1, '统一 Token 校验', 'HEADER_TOKEN', '{"allowedTokens":[]}', '通过 ZT-Auth-Token 传递访问凭证', 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(110100000000000002, 1, '内部服务白名单', 'INTERNAL_TRUSTED', '{"allowedTokens":["system-server","databus-server"]}', '内部系统间调用的白名单策略', 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(110100000000000003, 1, '调试临时凭证', 'HEADER_TOKEN', '{"allowedTokens":["debug-token"]}', '用于灰度测试的临时凭证', 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0);
-- Rate limit policies compatible with DefaultRateLimitPolicyEvaluator
INSERT INTO databus_policy_rate_limit
(id, tenant_id, name, type, config, description, creator, create_time, updater, update_time, deleted)
VALUES
(210100000000000001, 1, '公共查询 120 RPM', 'FIXED_WINDOW', '{"limit":120,"windowSeconds":60}', '公共查询接口的分钟级限流', 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(210100000000000002, 1, '用户画像 30 RPM', 'FIXED_WINDOW', '{"limit":30,"windowSeconds":60}', '用户画像聚合接口的限流策略', 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(210100000000000003, 1, '登录试用 60 RPM', 'FIXED_WINDOW', '{"limit":60,"windowSeconds":60}', '测试登录能力的限流', 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0);
-- API definitions referencing real system modules
INSERT INTO databus_api_definition
(id, tenant_id, api_code, uri_pattern, http_method, version, status, description, auth_policy_id, rate_limit_id, response_template, cache_strategy, updated_at, grey_released, creator, create_time, updater, update_time, deleted)
VALUES
(410100000000000001, 1, 'system.lookup.bundle', '/external/system/lookup-bundle', 'GET', 'v1', 1, '聚合系统用户、部门、字典精简列表的只读接口', 110100000000000001, 210100000000000001, '{"code":0,"message":"success","data":{}}', '{"provider":"redis","ttlSeconds":120,"cacheKey":"system:lookup:bundle"}', CURRENT_TIMESTAMP, 0, 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(410100000000000002, 1, 'system.user.profile.aggregate', '/external/system/user/profile', 'POST', 'v1', 1, '根据 userId 聚合后台用户、角色及部门信息', 110100000000000001, 210100000000000002, '{"code":0,"message":"success","data":{}}', NULL, CURRENT_TIMESTAMP, 0, 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(410100000000000003, 1, 'system.auth.quick-login', '/external/system/auth/quick-login', 'POST', 'v1', 1, '调用测试登录接口并返回用户基础画像', 110100000000000001, 210100000000000003, '{"code":0,"message":"success","data":{}}', NULL, CURRENT_TIMESTAMP, 1, 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0);
-- API level transforms
INSERT INTO databus_api_transform
(id, tenant_id, api_id, step_id, phase, expression_type, expression, description, creator, create_time, updater, update_time, deleted)
VALUES
(520100000000000101, 1, 410100000000000001, NULL, 'REQUEST_PRE', 'JSON', '($trace := $ctx.requestHeaders."X-Trace-Id"; {"requestHeaders": {"X-Trace-Id": $trace ? $trace : $uuid()}})', '自动补全链路追踪 ID', 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(520100000000000102, 1, 410100000000000001, NULL, 'RESPONSE_PRE', 'JSON', '{"responseBody": {"users": $vars.users ? $vars.users : [], "departments": $vars.departments ? $vars.departments : [], "dicts": $vars.dicts ? $vars.dicts : []}}', '组装统一响应结构', 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(520100000000000103, 1, 410100000000000002, NULL, 'RESPONSE_PRE', 'JSON', '{"responseBody": {"user": $vars.user, "roleIds": $vars.roleIds ? $vars.roleIds : [], "roles": $vars.roles ? $vars.roles : [], "departments": $vars.departments ? $vars.departments : []}}', '聚合用户详情返回体', 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(520100000000000104, 1, 410100000000000003, NULL, 'RESPONSE_PRE', 'JSON', '{"responseBody": {"tokens": $vars.tokens, "loginUser": $vars.loginUser, "companyDept": $vars.companyDept ? $vars.companyDept : []}}', '组合测试登录返回体', 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0);
-- API orchestration steps referencing real HTTP endpoints and Spring beans
INSERT INTO databus_api_step
(id, tenant_id, api_id, step_order, parallel_group, type, target_endpoint, request_mapping_expr, response_mapping_expr, transform_id, timeout, retry_strategy, fallback_strategy, condition_expr, stop_on_error, creator, create_time, updater, update_time, deleted)
VALUES
(610100000000000201, 1, 410100000000000001, 1, NULL, 'HTTP', 'GET http://127.0.0.1:48080/admin-api/system/user/list-all-simple', 'JSON::{}', 'JSON::($.code != 0 ? $error("调用 /admin-api/system/user/list-all-simple 失败: " & $.msg) : {"users": $.data ? $.data : []})', NULL, 5000, '{"maxAttempts":2,"delayMs":200}', NULL, NULL, 1, 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(610100000000000202, 1, 410100000000000001, 2, NULL, 'HTTP', 'GET http://127.0.0.1:48080/admin-api/system/dept/list-all-simple', 'JSON::{}', 'JSON::($.code != 0 ? $error("调用 /admin-api/system/dept/list-all-simple 失败: " & $.msg) : {"departments": $.data ? $.data : []})', NULL, 5000, '{"maxAttempts":2,"delayMs":200}', NULL, NULL, 1, 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(610100000000000203, 1, 410100000000000001, 3, NULL, 'HTTP', 'GET http://127.0.0.1:48080/admin-api/system/dict-data/list-all-simple', 'JSON::{}', 'JSON::($.code != 0 ? $error("调用 /admin-api/system/dict-data/list-all-simple 失败: " & $.msg) : {"dicts": $.data ? $.data : []})', NULL, 5000, '{"maxAttempts":2,"delayMs":200}', NULL, NULL, 1, 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(610100000000000204, 1, 410100000000000002, 1, NULL, 'HTTP', 'GET http://127.0.0.1:48080/admin-api/system/user/get', 'JSON::{"id": $.userId}', 'JSON::($.code != 0 ? $error("调用 /admin-api/system/user/get 失败: " & $.msg) : {"user": $.data})', NULL, 3000, '{"maxAttempts":1}', NULL, NULL, 1, 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(610100000000000205, 1, 410100000000000002, 2, NULL, 'HTTP', 'GET http://127.0.0.1:48080/admin-api/system/permission/list-user-roles', 'JSON::($user := $vars.user; {"userId": $user ? $user.id : null})', 'JSON::($.code != 0 ? $error("调用 /admin-api/system/permission/list-user-roles 失败: " & $.msg) : ($data := $.data ? $.data : []; {"roleIds": $data.($string($))}))', NULL, 3000, '{"maxAttempts":1}', NULL, NULL, 1, 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(610100000000000206, 1, 410100000000000002, 3, NULL, 'HTTP', 'GET http://127.0.0.1:48080/admin-api/system/role/list-all-simple', 'JSON::{}', 'JSON::($.code != 0 ? $error("调用 /admin-api/system/role/list-all-simple 失败: " & $.msg) : ($data := $.data ? $.data : []; {"roles": $data[$contains($vars.roleIds, $string(id))]}))', NULL, 3000, '{"maxAttempts":1}', NULL, NULL, 1, 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(610100000000000207, 1, 410100000000000002, 4, NULL, 'HTTP', 'GET http://127.0.0.1:48080/admin-api/system/dept/list-all-simple', 'JSON::{}', 'JSON::($.code != 0 ? $error("调用 /admin-api/system/dept/list-all-simple 失败: " & $.msg) : ($data := $.data ? $.data : []; $deptIds := $vars.user.deptIds ? $vars.user.deptIds.($string($)) : []; {"departments": $deptIds ? $data[$contains($deptIds, $string(id))] : []}))', NULL, 3000, '{"maxAttempts":1}', NULL, NULL, 1, 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(610100000000000208, 1, 410100000000000003, 1, NULL, 'HTTP', 'POST http://127.0.0.1:48080/admin-api/system/auth/test-login', 'JSON::{"username": $.username, "password": $.password}', 'JSON::($.code != 0 ? $error("调用 /admin-api/system/auth/test-login 失败: " & $.msg) : {"tokens": $.data})', NULL, 5000, '{"maxAttempts":1}', NULL, NULL, 1, 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(610100000000000209, 1, 410100000000000003, 2, NULL, 'HTTP', 'GET http://127.0.0.1:48080/admin-api/system/user/list-all-simple', 'JSON::{}', 'JSON::($.code != 0 ? $error("调用 /admin-api/system/user/list-all-simple 失败: " & $.msg) : ($data := $.data ? $.data : []; {"loginUser": $data[$string(id) = $string($vars.tokens.userId)][0]}))', NULL, 3000, '{"maxAttempts":1}', NULL, NULL, 1, 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
(610100000000000210, 1, 410100000000000003, 3, NULL, 'HTTP', 'GET http://127.0.0.1:48080/admin-api/system/dept/list-all-simple', 'JSON::{}', 'JSON::($.code != 0 ? $error("调用 /admin-api/system/dept/list-all-simple 失败: " & $.msg) : ($data := $.data ? $.data : []; {"companyDept": $vars.loginUser and $vars.loginUser.deptId ? $data[$string(id) = $string($vars.loginUser.deptId)] : []}))', NULL, 3000, '{"maxAttempts":1}', NULL, NULL, 1, 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0),
-- Optional: publish record to illustrate version management
INSERT INTO databus_api_flow_publish
(id, tenant_id, api_id, release_tag, snapshot, status, active, description, creator, create_time, updater, update_time, deleted)
VALUES
(710100000000000001, 1, 410100000000000001, '2025.10.01-INIT', '{"definitionId":410100000000000001,"version":"v1","steps":3}', 'SUCCESS', 1, '初始发布 system.lookup.bundle 接口', 'sample_loader', CURRENT_TIMESTAMP, 'sample_loader', CURRENT_TIMESTAMP, 0);
COMMIT;

View File

@@ -837,7 +837,7 @@ def main():
) )
args = parser.parse_args() args = parser.parse_args()
sql_file = pathlib.Path("../mysql/在线文档管理表结构.sql").resolve().as_posix() sql_file = pathlib.Path("../mysql/在线文档管理表结构_20250901.sql").resolve().as_posix()
convertor = None convertor = None
if args.type == "postgres": if args.type == "postgres":
convertor = PostgreSQLConvertor(sql_file) convertor = PostgreSQLConvertor(sql_file)

View File

@@ -186,6 +186,13 @@ spring:
- Path=/admin-api/crm/** - Path=/admin-api/crm/**
filters: filters:
- RewritePath=/admin-api/crm/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs - RewritePath=/admin-api/crm/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
## rule-server 服务
- id: rule-admin-api # 路由的编号
uri: grayLb://rule-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/admin-api/rule/**
filters:
- RewritePath=/admin-api/rule/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
## ai-server 服务 ## ai-server 服务
- id: ai-admin-api # 路由的编号 - id: ai-admin-api # 路由的编号
uri: grayLb://ai-server uri: grayLb://ai-server
@@ -207,6 +214,13 @@ spring:
- Path=/admin-api/template/** - Path=/admin-api/template/**
filters: filters:
- RewritePath=/admin-api/template/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs - RewritePath=/admin-api/template/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
## databus-server 服务
- id: databus-admin-api # 路由的编号
uri: grayLb://databus-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/admin-api/databus/**
filters:
- RewritePath=/admin-api/databus/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
x-forwarded: x-forwarded:
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀 prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀
@@ -258,6 +272,9 @@ knife4j:
- name: crm-server - name: crm-server
service-name: crm-server service-name: crm-server
url: /admin-api/crm/v3/api-docs url: /admin-api/crm/v3/api-docs
- name: rule-server
service-name: rule-server
url: /admin-api/rule/v3/api-docs
- name: ai-server - name: ai-server
service-name: ai-server service-name: ai-server
url: /admin-api/ai/v3/api-docs url: /admin-api/ai/v3/api-docs
@@ -267,6 +284,9 @@ knife4j:
- name: template-server - name: template-server
service-name: template-server service-name: template-server
url: /admin-api/template/v3/api-docs url: /admin-api/template/v3/api-docs
- name: databus-server
service-name: databus-server
url: /admin-api/databus/v3/api-docs
--- #################### 芋道相关配置 #################### --- #################### 芋道相关配置 ####################

View File

@@ -126,6 +126,63 @@
<artifactId>zt-spring-boot-starter-biz-business</artifactId> <artifactId>zt-spring-boot-starter-biz-business</artifactId>
<version>${revision}</version> <version>${revision}</version>
</dependency> </dependency>
<!-- Spring Integration & Flow Orchestration -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-http</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-scripting</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<!-- Reactive HTTP client for internal REST orchestration -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Expression evaluation & caching utilities -->
<dependency>
<groupId>com.ibm.jsonata4java</groupId>
<artifactId>JSONata4Java</artifactId>
<version>2.5.5</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.mvel</groupId>
<artifactId>mvel2</artifactId>
<version>2.5.2.Final</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<!-- Testing support -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>4.12.0</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -1,29 +0,0 @@
package com.zt.plat.module.databus.controller.admin.databus;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.zt.plat.framework.common.pojo.CommonResult;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
/**
* Databus 控制器
*
* @author ZT
*/
@Tag(name = "管理后台 - Databus")
@RestController
@RequestMapping("/admin/databus/databus")
public class DatabusController {
@GetMapping("/hello")
@Operation(summary = "Hello Databus")
public CommonResult<String> hello() {
return success("Hello, Databus!");
}
}

View File

@@ -0,0 +1,79 @@
package com.zt.plat.module.databus.controller.admin.gateway;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
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.ApiDefinitionConvert;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionDetailRespVO;
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.ApiDefinitionSummaryRespVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO;
import com.zt.plat.module.databus.framework.integration.gateway.core.IntegrationFlowManager;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.service.gateway.ApiDefinitionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_DEFINITION_NOT_FOUND;
@Tag(name = "管理后台 - API 定义管理")
@RestController
@RequestMapping("/databus/gateway/definition")
@RequiredArgsConstructor
@Validated
public class ApiDefinitionController {
private final ApiDefinitionService apiDefinitionService;
private final IntegrationFlowManager integrationFlowManager;
@GetMapping("/page")
@Operation(summary = "分页查询 API 定义")
public CommonResult<PageResult<ApiDefinitionSummaryRespVO>> getDefinitionPage(@Valid ApiDefinitionPageReqVO reqVO) {
PageResult<ApiDefinitionDO> pageResult = apiDefinitionService.getPage(reqVO);
return success(ApiDefinitionConvert.INSTANCE.convertPage(pageResult));
}
@GetMapping("/{id}")
@Operation(summary = "获取 API 定义详情")
public CommonResult<ApiDefinitionDetailRespVO> getDefinition(@PathVariable("id") Long id) {
ApiDefinitionAggregate aggregate = apiDefinitionService.findById(id)
.orElseThrow(() -> ServiceExceptionUtil.exception(API_DEFINITION_NOT_FOUND));
return success(ApiDefinitionConvert.INSTANCE.convert(aggregate));
}
@PostMapping
@Operation(summary = "创建 API 定义")
public CommonResult<Long> createDefinition(@Valid @RequestBody ApiDefinitionSaveReqVO reqVO) {
Long id = apiDefinitionService.create(reqVO);
integrationFlowManager.refresh(reqVO.getApiCode(), reqVO.getVersion());
return success(id);
}
@PutMapping
@Operation(summary = "更新 API 定义")
public CommonResult<Boolean> updateDefinition(@Valid @RequestBody ApiDefinitionSaveReqVO reqVO) {
ApiDefinitionAggregate before = apiDefinitionService.findById(reqVO.getId())
.orElseThrow(() -> ServiceExceptionUtil.exception(API_DEFINITION_NOT_FOUND));
apiDefinitionService.update(reqVO);
integrationFlowManager.refresh(before.getDefinition().getApiCode(), before.getDefinition().getVersion());
integrationFlowManager.refresh(reqVO.getApiCode(), reqVO.getVersion());
return success(Boolean.TRUE);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除 API 定义")
public CommonResult<Boolean> deleteDefinition(@PathVariable("id") Long id) {
ApiDefinitionAggregate aggregate = apiDefinitionService.findById(id)
.orElseThrow(() -> ServiceExceptionUtil.exception(API_DEFINITION_NOT_FOUND));
apiDefinitionService.delete(id);
integrationFlowManager.refresh(aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion());
return success(Boolean.TRUE);
}
}

View File

@@ -0,0 +1,107 @@
package com.zt.plat.module.databus.controller.admin.gateway;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.module.databus.controller.admin.gateway.convert.ApiDefinitionConvert;
import com.zt.plat.module.databus.controller.admin.gateway.vo.ApiGatewayInvokeReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionDetailRespVO;
import com.zt.plat.module.databus.framework.integration.gateway.core.ApiFlowDispatcher;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import com.zt.plat.module.databus.service.gateway.ApiDefinitionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - API 门户")
@RestController
@RequestMapping("/databus/gateway")
@RequiredArgsConstructor
public class ApiGatewayController {
private final ApiFlowDispatcher apiFlowDispatcher;
private final ApiDefinitionService apiDefinitionService;
@PostMapping(value = "/invoke", consumes = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "测试调用 API 编排")
public CommonResult<ApiGatewayResponse> invoke(@RequestBody ApiGatewayInvokeReqVO reqVO) {
ApiInvocationContext context = ApiInvocationContext.create();
context.setApiCode(reqVO.getApiCode());
context.setApiVersion(reqVO.getVersion());
context.setRequestBody(reqVO.getPayload());
if (reqVO.getHeaders() != null) {
context.getRequestHeaders().putAll(reqVO.getHeaders());
}
if (reqVO.getQueryParams() != null) {
context.getRequestQueryParams().putAll(reqVO.getQueryParams());
}
ApiInvocationContext responseContext = context;
try {
responseContext = apiFlowDispatcher.dispatch(reqVO.getApiCode(), reqVO.getVersion(), context);
} catch (ServiceException ex) {
handleServiceException(responseContext, ex);
} catch (Exception ex) {
handleUnexpectedException(responseContext, ex);
}
int status = responseContext.getResponseStatus() != null ? responseContext.getResponseStatus() : HttpStatus.OK.value();
String message = StringUtils.hasText(responseContext.getResponseMessage())
? responseContext.getResponseMessage()
: HttpStatus.valueOf(status).getReasonPhrase();
ApiGatewayResponse envelope = ApiGatewayResponse.builder()
.code(status >= 200 && status < 400 ? "SUCCESS" : "ERROR")
.message(message)
.data(responseContext.getResponseBody())
.traceId(responseContext.getRequestId())
.build();
return success(envelope);
}
@GetMapping("/definitions")
@Operation(summary = "获取当前已发布 API 配置")
public CommonResult<List<ApiDefinitionDetailRespVO>> listDefinitions() {
List<ApiDefinitionDetailRespVO> definitions = apiDefinitionService.loadActiveDefinitions().stream()
.map(ApiDefinitionConvert.INSTANCE::convert)
.collect(Collectors.toList());
return success(definitions);
}
private void handleServiceException(ApiInvocationContext context, ServiceException ex) {
String message = StringUtils.hasText(ex.getMessage()) ? ex.getMessage() : "API 调用失败";
context.setResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
context.setResponseMessage(message);
Map<String, Object> body = new HashMap<>();
if (ex.getCode() != null) {
body.put("errorCode", ex.getCode());
}
body.put("errorMessage", message);
context.setResponseBody(body);
}
private void handleUnexpectedException(ApiInvocationContext context, Exception ex) {
String message = StringUtils.hasText(ex.getMessage())
? ex.getMessage()
: ex.getCause() != null && StringUtils.hasText(ex.getCause().getMessage())
? ex.getCause().getMessage()
: "API invocation encountered an unexpected error";
context.setResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
context.setResponseMessage(message);
Map<String, Object> body = new HashMap<>();
body.put("errorMessage", message);
body.put("exception", ex.getClass().getSimpleName());
context.setResponseBody(body);
}
}

View File

@@ -0,0 +1,84 @@
package com.zt.plat.module.databus.controller.admin.gateway;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
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.ApiPolicyAuthConvert;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyPageReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyRespVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySaveReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySimpleRespVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO;
import com.zt.plat.module.databus.service.gateway.ApiPolicyAuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_POLICY_NOT_FOUND;
@Tag(name = "管理后台 - 网关认证策略")
@RestController
@RequestMapping("/databus/gateway/policy/auth")
@RequiredArgsConstructor
@Validated
public class ApiPolicyAuthController {
private final ApiPolicyAuthService authService;
@GetMapping("/page")
@Operation(summary = "分页查询认证策略")
public CommonResult<PageResult<ApiPolicyRespVO>> getAuthPolicyPage(@Valid ApiPolicyPageReqVO reqVO) {
PageResult<ApiPolicyAuthDO> pageResult = authService.getPage(reqVO);
return success(ApiPolicyAuthConvert.INSTANCE.convertPage(pageResult));
}
@GetMapping("/{id}")
@Operation(summary = "查询认证策略详情")
public CommonResult<ApiPolicyRespVO> getAuthPolicy(@PathVariable("id") Long id) {
ApiPolicyAuthDO policy = authService.get(id)
.orElseThrow(() -> ServiceExceptionUtil.exception(API_POLICY_NOT_FOUND));
return success(ApiPolicyAuthConvert.INSTANCE.convert(policy));
}
@GetMapping("/simple-list")
@Operation(summary = "获取认证策略精简列表")
public CommonResult<List<ApiPolicySimpleRespVO>> getAuthPolicySimpleList() {
List<ApiPolicyAuthDO> list = authService.getSimpleList();
return success(ApiPolicyAuthConvert.INSTANCE.convertSimpleList(list));
}
@PostMapping
@Operation(summary = "创建认证策略")
public CommonResult<Long> createAuthPolicy(@Valid @RequestBody ApiPolicySaveReqVO reqVO) {
Long id = authService.create(reqVO);
return success(id);
}
@PutMapping
@Operation(summary = "更新认证策略")
public CommonResult<Boolean> updateAuthPolicy(@Valid @RequestBody ApiPolicySaveReqVO reqVO) {
authService.update(reqVO);
return success(Boolean.TRUE);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除认证策略")
public CommonResult<Boolean> deleteAuthPolicy(@PathVariable("id") Long id) {
authService.delete(id);
return success(Boolean.TRUE);
}
}

View File

@@ -0,0 +1,84 @@
package com.zt.plat.module.databus.controller.admin.gateway;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
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.ApiPolicyRateLimitConvert;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyPageReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyRespVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySaveReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySimpleRespVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO;
import com.zt.plat.module.databus.service.gateway.ApiPolicyRateLimitService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_POLICY_NOT_FOUND;
@Tag(name = "管理后台 - 网关限流策略")
@RestController
@RequestMapping("/databus/gateway/policy/rate-limit")
@RequiredArgsConstructor
@Validated
public class ApiPolicyRateLimitController {
private final ApiPolicyRateLimitService rateLimitService;
@GetMapping("/page")
@Operation(summary = "分页查询限流策略")
public CommonResult<PageResult<ApiPolicyRespVO>> getRateLimitPolicyPage(@Valid ApiPolicyPageReqVO reqVO) {
PageResult<ApiPolicyRateLimitDO> pageResult = rateLimitService.getPage(reqVO);
return success(ApiPolicyRateLimitConvert.INSTANCE.convertPage(pageResult));
}
@GetMapping("/{id}")
@Operation(summary = "查询限流策略详情")
public CommonResult<ApiPolicyRespVO> getRateLimitPolicy(@PathVariable("id") Long id) {
ApiPolicyRateLimitDO policy = rateLimitService.get(id)
.orElseThrow(() -> ServiceExceptionUtil.exception(API_POLICY_NOT_FOUND));
return success(ApiPolicyRateLimitConvert.INSTANCE.convert(policy));
}
@GetMapping("/simple-list")
@Operation(summary = "获取限流策略精简列表")
public CommonResult<List<ApiPolicySimpleRespVO>> getRateLimitPolicySimpleList() {
List<ApiPolicyRateLimitDO> list = rateLimitService.getSimpleList();
return success(ApiPolicyRateLimitConvert.INSTANCE.convertSimpleList(list));
}
@PostMapping
@Operation(summary = "创建限流策略")
public CommonResult<Long> createRateLimitPolicy(@Valid @RequestBody ApiPolicySaveReqVO reqVO) {
Long id = rateLimitService.create(reqVO);
return success(id);
}
@PutMapping
@Operation(summary = "更新限流策略")
public CommonResult<Boolean> updateRateLimitPolicy(@Valid @RequestBody ApiPolicySaveReqVO reqVO) {
rateLimitService.update(reqVO);
return success(Boolean.TRUE);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除限流策略")
public CommonResult<Boolean> deleteRateLimitPolicy(@PathVariable("id") Long id) {
rateLimitService.delete(id);
return success(Boolean.TRUE);
}
}

View File

@@ -0,0 +1,104 @@
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.dal.dataobject.gateway.ApiDefinitionDO;
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 org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@Mapper
public interface ApiDefinitionConvert {
ApiDefinitionConvert INSTANCE = Mappers.getMapper(ApiDefinitionConvert.class);
ApiDefinitionSummaryRespVO convert(ApiDefinitionDO bean);
List<ApiDefinitionSummaryRespVO> convertList(List<ApiDefinitionDO> list);
default PageResult<ApiDefinitionSummaryRespVO> convertPage(PageResult<ApiDefinitionDO> page) {
if (page == null) {
return PageResult.empty();
}
PageResult<ApiDefinitionSummaryRespVO> result = new PageResult<>();
List<ApiDefinitionSummaryRespVO> list = convertList(page.getList());
result.setList(list == null ? new ArrayList<>() : list);
result.setTotal(page.getTotal());
return result;
}
default ApiDefinitionDetailRespVO convert(ApiDefinitionAggregate aggregate) {
if (aggregate == null) {
return null;
}
ApiDefinitionDetailRespVO detail = BeanUtils.toBean(aggregate.getDefinition(), ApiDefinitionDetailRespVO.class);
detail.setApiLevelTransforms(convertTransforms(aggregate.getDefinition().getId(), aggregate.getApiLevelTransforms().values()));
detail.setSteps(convertSteps(aggregate.getSteps()));
detail.setPublication(convert(aggregate.getPublication()));
return detail;
}
default List<ApiDefinitionStepRespVO> convertSteps(List<ApiStepDefinition> steps) {
if (CollUtil.isEmpty(steps)) {
return new ArrayList<>();
}
return steps.stream()
.sorted(Comparator.comparing(step -> step.getStep().getStepOrder() == null ? Integer.MAX_VALUE : step.getStep().getStepOrder()))
.map(step -> {
ApiDefinitionStepRespVO resp = BeanUtils.toBean(step.getStep(), ApiDefinitionStepRespVO.class);
resp.setTransforms(convertStepTransforms(step.getStep().getApiId(), step.getStep().getId(), step.getTransforms()));
return resp;
})
.collect(Collectors.toList());
}
default List<ApiDefinitionTransformRespVO> convertTransforms(Long apiId, Collection<ApiTransformDefinition> transforms) {
if (CollUtil.isEmpty(transforms)) {
return new ArrayList<>();
}
return transforms.stream()
.sorted(Comparator.comparing(ApiTransformDefinition::getPhase, Comparator.nullsLast(String::compareTo)))
.map(transform -> {
ApiDefinitionTransformRespVO resp = BeanUtils.toBean(transform, ApiDefinitionTransformRespVO.class);
resp.setApiId(apiId);
resp.setStepId(null);
return resp;
})
.collect(Collectors.toList());
}
default List<ApiDefinitionTransformRespVO> convertStepTransforms(Long apiId, Long stepId, List<ApiTransformDefinition> transforms) {
if (CollUtil.isEmpty(transforms)) {
return new ArrayList<>();
}
return transforms.stream()
.sorted(Comparator.comparing(ApiTransformDefinition::getPhase, Comparator.nullsLast(String::compareTo)))
.map(transform -> {
ApiDefinitionTransformRespVO resp = BeanUtils.toBean(transform, ApiDefinitionTransformRespVO.class);
resp.setApiId(apiId);
resp.setStepId(stepId);
return resp;
})
.collect(Collectors.toList());
}
default ApiDefinitionPublicationRespVO convert(ApiFlowPublication publication) {
return publication == null ? null : BeanUtils.toBean(publication, ApiDefinitionPublicationRespVO.class);
}
}

View File

@@ -0,0 +1,25 @@
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.policy.ApiPolicyRespVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySimpleRespVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper
public interface ApiPolicyAuthConvert {
ApiPolicyAuthConvert INSTANCE = Mappers.getMapper(ApiPolicyAuthConvert.class);
ApiPolicyRespVO convert(ApiPolicyAuthDO bean);
List<ApiPolicyRespVO> convertList(List<ApiPolicyAuthDO> list);
PageResult<ApiPolicyRespVO> convertPage(PageResult<ApiPolicyAuthDO> page);
List<ApiPolicySimpleRespVO> convertSimpleList(List<ApiPolicyAuthDO> list);
}

View File

@@ -0,0 +1,25 @@
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.policy.ApiPolicyRespVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySimpleRespVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper
public interface ApiPolicyRateLimitConvert {
ApiPolicyRateLimitConvert INSTANCE = Mappers.getMapper(ApiPolicyRateLimitConvert.class);
ApiPolicyRespVO convert(ApiPolicyRateLimitDO bean);
List<ApiPolicyRespVO> convertList(List<ApiPolicyRateLimitDO> list);
PageResult<ApiPolicyRespVO> convertPage(PageResult<ApiPolicyRateLimitDO> page);
List<ApiPolicySimpleRespVO> convertSimpleList(List<ApiPolicyRateLimitDO> list);
}

View File

@@ -0,0 +1,27 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
public class ApiGatewayInvokeReqVO {
@Schema(description = "API 编码", requiredMode = Schema.RequiredMode.REQUIRED)
private String apiCode;
@Schema(description = "API 版本", requiredMode = Schema.RequiredMode.REQUIRED)
private String version;
@Schema(description = "请求头,可选")
private Map<String, String> headers = new HashMap<>();
@Schema(description = "请求参数,可选")
private Map<String, Object> queryParams = new HashMap<>();
@Schema(description = "请求体")
private Object payload;
}

View File

@@ -0,0 +1,74 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "管理后台 - API 定义详情 Response VO")
public class ApiDefinitionDetailRespVO {
@Schema(description = "主键", example = "1024")
private Long id;
@Schema(description = "租户标识", example = "1")
private String tenantId;
@Schema(description = "API 编码", example = "order.create")
private String apiCode;
@Schema(description = "API 版本", example = "v1")
private String version;
@Schema(description = "HTTP 方法", example = "POST")
private String httpMethod;
@Schema(description = "URI 模板", example = "/external/order/create")
private String uriPattern;
@Schema(description = "状态", example = "1")
private Integer status;
@Schema(description = "是否灰度")
private Boolean greyReleased;
@Schema(description = "描述")
private String description;
@Schema(description = "认证策略编号")
private Long authPolicyId;
@Schema(description = "限流策略编号")
private Long rateLimitId;
@Schema(description = "响应模板(JSON)")
private String responseTemplate;
@Schema(description = "缓存策略(JSON)")
private String cacheStrategy;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@Schema(description = "创建人")
private String creator;
@Schema(description = "更新人")
private String updater;
@Schema(description = "API 级别变换列表")
private List<ApiDefinitionTransformRespVO> apiLevelTransforms = new ArrayList<>();
@Schema(description = "步骤列表")
private List<ApiDefinitionStepRespVO> steps = new ArrayList<>();
@Schema(description = "发布信息")
private ApiDefinitionPublicationRespVO publication;
}

View File

@@ -0,0 +1,25 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import com.zt.plat.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "管理后台 - API 定义分页查询 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiDefinitionPageReqVO extends PageParam {
@Schema(description = "关键字,匹配编码/描述/URI", example = "order")
private String keyword;
@Schema(description = "API 状态", example = "1")
private Integer status;
@Schema(description = "HTTP 方法", example = "POST")
private String httpMethod;
@Schema(description = "是否灰度", example = "true")
private Boolean greyReleased;
}

View File

@@ -0,0 +1,28 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "管理后台 - API 发布信息 Response VO")
public class ApiDefinitionPublicationRespVO {
@Schema(description = "发布记录主键", example = "4001")
private Long id;
@Schema(description = "发布标签", example = "release-20231001")
private String releaseTag;
@Schema(description = "快照内容(JSON)")
private String snapshot;
@Schema(description = "状态", example = "RELEASED")
private String status;
@Schema(description = "是否当前生效")
private Boolean active;
@Schema(description = "描述")
private String description;
}

View File

@@ -0,0 +1,67 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "管理后台 - API 定义保存 Request VO")
public class ApiDefinitionSaveReqVO {
@Schema(description = "主键", example = "1001")
private Long id;
@Schema(description = "API 编码", example = "order.create")
@NotBlank(message = "API 编码不能为空")
private String apiCode;
@Schema(description = "API 版本", example = "v1")
@NotBlank(message = "API 版本不能为空")
private String version;
@Schema(description = "HTTP 方法", example = "POST")
@NotBlank(message = "HTTP 方法不能为空")
private String httpMethod;
@Schema(description = "URI 模板", example = "/external/order/create")
@NotBlank(message = "URI 模板不能为空")
private String uriPattern;
@Schema(description = "API 状态", example = "1")
@NotNull(message = "API 状态不能为空")
private Integer status;
@Schema(description = "描述")
private String description;
@Schema(description = "认证策略编号")
private Long authPolicyId;
@Schema(description = "限流策略编号")
private Long rateLimitId;
@Schema(description = "响应模板(JSON)")
private String responseTemplate;
@Schema(description = "缓存策略(JSON)")
private String cacheStrategy;
@Schema(description = "是否开启灰度发布")
private Boolean greyReleased;
@Schema(description = "API 级别变换列表")
@Valid
private List<ApiDefinitionTransformSaveReqVO> apiLevelTransforms = new ArrayList<>();
@Schema(description = "步骤列表")
@NotEmpty(message = "编排步骤不能为空")
@Valid
private List<ApiDefinitionStepSaveReqVO> steps = new ArrayList<>();
}

View File

@@ -0,0 +1,55 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "管理后台 - API 编排步骤详情 Response VO")
public class ApiDefinitionStepRespVO {
@Schema(description = "步骤主键", example = "21001")
private Long id;
@Schema(description = "所属 API 主键", example = "1024")
private Long apiId;
@Schema(description = "步骤序号", example = "1")
private Integer stepOrder;
@Schema(description = "并行分组")
private String parallelGroup;
@Schema(description = "步骤类型", example = "HTTP")
private String type;
@Schema(description = "目标端点")
private String targetEndpoint;
@Schema(description = "请求映射表达式(JSON)")
private String requestMappingExpr;
@Schema(description = "响应映射表达式(JSON)")
private String responseMappingExpr;
@Schema(description = "超时时间(毫秒)")
private Long timeout;
@Schema(description = "重试策略(JSON)")
private String retryStrategy;
@Schema(description = "降级策略(JSON)")
private String fallbackStrategy;
@Schema(description = "条件表达式")
private String conditionExpr;
@Schema(description = "是否出错终止")
private Boolean stopOnError;
@Schema(description = "步骤级变换列表")
private List<ApiDefinitionTransformRespVO> transforms = new ArrayList<>();
}

View File

@@ -0,0 +1,58 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "管理后台 - API 编排步骤保存 Request VO")
public class ApiDefinitionStepSaveReqVO {
@Schema(description = "步骤主键", example = "21001")
private Long id;
@Schema(description = "步骤序号", example = "1")
@NotNull(message = "步骤序号不能为空")
private Integer stepOrder;
@Schema(description = "并行分组")
private String parallelGroup;
@Schema(description = "步骤类型", example = "HTTP")
@NotBlank(message = "步骤类型不能为空")
private String type;
@Schema(description = "目标端点", example = "https://api.demo.com/order")
private String targetEndpoint;
@Schema(description = "请求映射表达式(JSON)")
private String requestMappingExpr;
@Schema(description = "响应映射表达式(JSON)")
private String responseMappingExpr;
@Schema(description = "超时时间(毫秒)", example = "5000")
private Long timeout;
@Schema(description = "重试策略(JSON)")
private String retryStrategy;
@Schema(description = "降级策略(JSON)")
private String fallbackStrategy;
@Schema(description = "条件表达式")
private String conditionExpr;
@Schema(description = "是否出错终止")
private Boolean stopOnError;
@Schema(description = "步骤级变换列表")
@Valid
private List<ApiDefinitionTransformSaveReqVO> transforms = new ArrayList<>();
}

View File

@@ -0,0 +1,48 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "管理后台 - API 定义分页列表 Response VO")
public class ApiDefinitionSummaryRespVO {
@Schema(description = "主键", example = "1024")
private Long id;
@Schema(description = "API 编码", example = "order.create")
private String apiCode;
@Schema(description = "API 版本", example = "v1")
private String version;
@Schema(description = "HTTP 方法", example = "POST")
private String httpMethod;
@Schema(description = "URI 模板", example = "/external/order/create")
private String uriPattern;
@Schema(description = "状态", example = "1")
private Integer status;
@Schema(description = "是否灰度", example = "true")
private Boolean greyReleased;
@Schema(description = "描述")
private String description;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@Schema(description = "创建人")
private String creator;
@Schema(description = "更新人")
private String updater;
}

View File

@@ -0,0 +1,31 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "管理后台 - API 变换详情 Response VO")
public class ApiDefinitionTransformRespVO {
@Schema(description = "变换主键", example = "31001")
private Long id;
@Schema(description = "所属 API 主键", example = "1024")
private Long apiId;
@Schema(description = "所属步骤主键", example = "21001")
private Long stepId;
@Schema(description = "阶段", example = "REQUEST")
private String phase;
@Schema(description = "表达式类型", example = "SPEL")
private String expressionType;
@Schema(description = "表达式内容", example = "#{payload}")
private String expression;
@Schema(description = "描述")
private String description;
}

View File

@@ -0,0 +1,29 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "管理后台 - API 变换保存 Request VO")
public class ApiDefinitionTransformSaveReqVO {
@Schema(description = "变换主键", example = "31001")
private Long id;
@Schema(description = "阶段", example = "REQUEST")
@NotBlank(message = "变换阶段不能为空")
private String phase;
@Schema(description = "表达式类型", example = "SPEL")
@NotBlank(message = "表达式类型不能为空")
private String expressionType;
@Schema(description = "表达式内容", example = "#{payload}")
@NotBlank(message = "表达式内容不能为空")
private String expression;
@Schema(description = "描述")
private String description;
}

View File

@@ -0,0 +1,27 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.policy;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* Base VO for policy definitions shared by request/response objects.
*/
@Data
public class ApiPolicyBaseVO {
@Schema(description = "策略名称", example = "JWT")
@NotBlank(message = "策略名称不能为空")
private String name;
@Schema(description = "策略类型", example = "JWT")
@NotBlank(message = "策略类型不能为空")
private String type;
@Schema(description = "策略配置(JSON)", example = "{\"issuer\":\"iam\"}")
private String config;
@Schema(description = "策略描述", example = "JWT 认证策略")
private String description;
}

View File

@@ -0,0 +1,22 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.policy;
import com.zt.plat.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Policy search conditions with pagination.
*/
@Schema(description = "管理后台 - 策略分页查询 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiPolicyPageReqVO extends PageParam {
@Schema(description = "关键字(名称/描述)", example = "JWT")
private String keyword;
@Schema(description = "策略类型", example = "JWT")
private String type;
}

View File

@@ -0,0 +1,32 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.policy;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* Policy detail response VO.
*/
@Schema(description = "管理后台 - 策略详情 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiPolicyRespVO extends ApiPolicyBaseVO {
@Schema(description = "策略编号", example = "1024")
private Long id;
@Schema(description = "创建人", example = "admin")
private String creator;
@Schema(description = "修改人", example = "admin")
private String updater;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "最后更新时间")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,18 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.policy;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Policy create/update request VO.
*/
@Schema(description = "管理后台 - 策略保存 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiPolicySaveReqVO extends ApiPolicyBaseVO {
@Schema(description = "策略编号", example = "1024")
private Long id;
}

View File

@@ -0,0 +1,25 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.policy;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Policy simple response VO used by dropdowns.
*/
@Schema(description = "管理后台 - 策略精简 Response VO")
@Data
public class ApiPolicySimpleRespVO {
@Schema(description = "策略编号", example = "1024")
private Long id;
@Schema(description = "策略名称", example = "JWT")
private String name;
@Schema(description = "策略类型", example = "JWT")
private String type;
@Schema(description = "策略描述")
private String description;
}

View File

@@ -0,0 +1,52 @@
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;
/**
* API definition data object describing external API metadata and policies.
*/
@TableName("databus_api_definition")
@KeySequence("databus_api_definition_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiDefinitionDO extends TenantBaseDO {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String apiCode;
private String uriPattern;
private String httpMethod;
private String version;
/**
* API status, see {@code ApiPublishStatusEnum}.
*/
private Integer status;
private String description;
private Long authPolicyId;
private Long rateLimitId;
private String responseTemplate;
private String cacheStrategy;
private LocalDateTime updatedAt;
private Boolean greyReleased;
}

View File

@@ -0,0 +1,35 @@
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;
/**
* Publication record for API flow snapshots and gray releases.
*/
@TableName("databus_api_flow_publish")
@KeySequence("databus_api_flow_publish_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiFlowPublishDO extends TenantBaseDO {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long apiId;
private String releaseTag;
private String snapshot;
private String status;
private Boolean active;
private String description;
}

View File

@@ -0,0 +1,31 @@
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;
/**
* Authentication policy definition.
*/
@TableName("databus_policy_auth")
@KeySequence("databus_policy_auth_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiPolicyAuthDO extends TenantBaseDO {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
private String type;
private String config;
private String description;
}

View File

@@ -0,0 +1,31 @@
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;
/**
* Rate limit policy definition stored in database.
*/
@TableName("databus_policy_rate_limit")
@KeySequence("databus_policy_rate_limit_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiPolicyRateLimitDO extends TenantBaseDO {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
private String type;
private String config;
private String description;
}

View File

@@ -0,0 +1,49 @@
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 orchestration step definition.
*/
@TableName("databus_api_step")
@KeySequence("databus_api_step_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiStepDO extends TenantBaseDO {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long apiId;
private Integer stepOrder;
private String parallelGroup;
private String type;
private String targetEndpoint;
private String requestMappingExpr;
private String responseMappingExpr;
private Long transformId;
private Long timeout;
private String retryStrategy;
private String fallbackStrategy;
private String conditionExpr;
private Boolean stopOnError;
}

View File

@@ -0,0 +1,35 @@
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 request/response transformation expressions.
*/
@TableName("databus_api_transform")
@KeySequence("databus_api_transform_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiTransformDO extends TenantBaseDO {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long apiId;
private Long stepId;
private String phase;
private String expressionType;
private String expression;
private String description;
}

View File

@@ -0,0 +1,63 @@
package com.zt.plat.module.databus.dal.mysql.gateway;
import cn.hutool.core.util.StrUtil;
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.definition.ApiDefinitionPageReqVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import java.util.Optional;
@Mapper
public interface ApiDefinitionMapper extends BaseMapperX<ApiDefinitionDO> {
default Optional<ApiDefinitionDO> selectByCodeAndVersion(String apiCode, String version) {
return Optional.ofNullable(selectOne(ApiDefinitionDO::getApiCode, apiCode,
ApiDefinitionDO::getVersion, version,
ApiDefinitionDO::getDeleted, false));
}
default List<ApiDefinitionDO> selectActiveDefinitions(List<Integer> statusList) {
return selectList(new LambdaQueryWrapperX<ApiDefinitionDO>()
.inIfPresent(ApiDefinitionDO::getStatus, statusList)
.eq(ApiDefinitionDO::getDeleted, false));
}
default PageResult<ApiDefinitionDO> selectPage(ApiDefinitionPageReqVO reqVO) {
LambdaQueryWrapperX<ApiDefinitionDO> query = new LambdaQueryWrapperX<>();
if (StrUtil.isNotBlank(reqVO.getKeyword())) {
String keyword = reqVO.getKeyword();
query.and(wrapper -> wrapper.like(ApiDefinitionDO::getApiCode, keyword)
.or().like(ApiDefinitionDO::getDescription, keyword)
.or().like(ApiDefinitionDO::getUriPattern, keyword));
}
query.eqIfPresent(ApiDefinitionDO::getStatus, reqVO.getStatus())
.eqIfPresent(ApiDefinitionDO::getHttpMethod, reqVO.getHttpMethod())
// .eqIfPresent(ApiDefinitionDO::getGreyReleased, reqVO.getGreyReleased())
.orderByDesc(ApiDefinitionDO::getUpdateTime)
.orderByDesc(ApiDefinitionDO::getId);
return selectPage(reqVO, query);
}
default Long selectCountByAuthPolicyId(Long policyId) {
if (policyId == null) {
return 0L;
}
return selectCount(new LambdaQueryWrapperX<ApiDefinitionDO>()
.eq(ApiDefinitionDO::getAuthPolicyId, policyId)
.eq(ApiDefinitionDO::getDeleted, false));
}
default Long selectCountByRateLimitPolicyId(Long policyId) {
if (policyId == null) {
return 0L;
}
return selectCount(new LambdaQueryWrapperX<ApiDefinitionDO>()
.eq(ApiDefinitionDO::getRateLimitId, policyId)
.eq(ApiDefinitionDO::getDeleted, false));
}
}

View File

@@ -0,0 +1,19 @@
package com.zt.plat.module.databus.dal.mysql.gateway;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiFlowPublishDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Optional;
@Mapper
public interface ApiFlowPublishMapper extends BaseMapperX<ApiFlowPublishDO> {
default Optional<ApiFlowPublishDO> selectActiveByApiId(Long apiId) {
return Optional.ofNullable(selectOne(new LambdaQueryWrapperX<ApiFlowPublishDO>()
.eq(ApiFlowPublishDO::getApiId, apiId)
.eq(ApiFlowPublishDO::getActive, true)));
}
}

View File

@@ -0,0 +1,36 @@
package com.zt.plat.module.databus.dal.mysql.gateway;
import cn.hutool.core.util.StrUtil;
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.policy.ApiPolicyPageReqVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface ApiPolicyAuthMapper extends BaseMapperX<ApiPolicyAuthDO> {
default PageResult<ApiPolicyAuthDO> selectPage(ApiPolicyPageReqVO reqVO) {
LambdaQueryWrapperX<ApiPolicyAuthDO> query = new LambdaQueryWrapperX<>();
if (StrUtil.isNotBlank(reqVO.getKeyword())) {
String keyword = reqVO.getKeyword();
query.and(wrapper -> wrapper.like(ApiPolicyAuthDO::getName, keyword)
.or().like(ApiPolicyAuthDO::getDescription, keyword));
}
query.eqIfPresent(ApiPolicyAuthDO::getType, reqVO.getType())
.eq(ApiPolicyAuthDO::getDeleted, false)
.orderByDesc(ApiPolicyAuthDO::getUpdateTime)
.orderByDesc(ApiPolicyAuthDO::getId);
return selectPage(reqVO, query);
}
default List<ApiPolicyAuthDO> selectSimpleList() {
return selectList(new LambdaQueryWrapperX<ApiPolicyAuthDO>()
.eq(ApiPolicyAuthDO::getDeleted, false)
.orderByDesc(ApiPolicyAuthDO::getUpdateTime)
.orderByDesc(ApiPolicyAuthDO::getId));
}
}

View File

@@ -0,0 +1,36 @@
package com.zt.plat.module.databus.dal.mysql.gateway;
import cn.hutool.core.util.StrUtil;
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.policy.ApiPolicyPageReqVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface ApiPolicyRateLimitMapper extends BaseMapperX<ApiPolicyRateLimitDO> {
default PageResult<ApiPolicyRateLimitDO> selectPage(ApiPolicyPageReqVO reqVO) {
LambdaQueryWrapperX<ApiPolicyRateLimitDO> query = new LambdaQueryWrapperX<>();
if (StrUtil.isNotBlank(reqVO.getKeyword())) {
String keyword = reqVO.getKeyword();
query.and(wrapper -> wrapper.like(ApiPolicyRateLimitDO::getName, keyword)
.or().like(ApiPolicyRateLimitDO::getDescription, keyword));
}
query.eqIfPresent(ApiPolicyRateLimitDO::getType, reqVO.getType())
.eq(ApiPolicyRateLimitDO::getDeleted, false)
.orderByDesc(ApiPolicyRateLimitDO::getUpdateTime)
.orderByDesc(ApiPolicyRateLimitDO::getId);
return selectPage(reqVO, query);
}
default List<ApiPolicyRateLimitDO> selectSimpleList() {
return selectList(new LambdaQueryWrapperX<ApiPolicyRateLimitDO>()
.eq(ApiPolicyRateLimitDO::getDeleted, false)
.orderByDesc(ApiPolicyRateLimitDO::getUpdateTime)
.orderByDesc(ApiPolicyRateLimitDO::getId));
}
}

View File

@@ -0,0 +1,25 @@
package com.zt.plat.module.databus.dal.mysql.gateway;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiStepDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface ApiStepMapper extends BaseMapperX<ApiStepDO> {
default List<ApiStepDO> selectByApiId(Long apiId) {
return selectList(new LambdaQueryWrapperX<ApiStepDO>()
.eq(ApiStepDO::getApiId, apiId)
.orderByAsc(ApiStepDO::getParallelGroup)
.orderByAsc(ApiStepDO::getStepOrder));
}
default void deleteByApiId(Long apiId) {
delete(new LambdaQueryWrapperX<ApiStepDO>()
.eq(ApiStepDO::getApiId, apiId));
}
}

View File

@@ -0,0 +1,39 @@
package com.zt.plat.module.databus.dal.mysql.gateway;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiTransformDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface ApiTransformMapper extends BaseMapperX<ApiTransformDO> {
default List<ApiTransformDO> selectByApiId(Long apiId) {
return selectList(new LambdaQueryWrapperX<ApiTransformDO>()
.eq(ApiTransformDO::getApiId, apiId));
}
default List<ApiTransformDO> selectByStepId(Long stepId) {
return selectList(new LambdaQueryWrapperX<ApiTransformDO>()
.eq(ApiTransformDO::getStepId, stepId));
}
default List<ApiTransformDO> selectApiLevelTransforms(Long apiId) {
return selectList(new LambdaQueryWrapperX<ApiTransformDO>()
.eq(ApiTransformDO::getApiId, apiId)
.isNull(ApiTransformDO::getStepId));
}
default void deleteByApiId(Long apiId) {
delete(new LambdaQueryWrapperX<ApiTransformDO>()
.eq(ApiTransformDO::getApiId, apiId));
}
default void deleteByStepId(Long stepId) {
delete(new LambdaQueryWrapperX<ApiTransformDO>()
.eq(ApiTransformDO::getStepId, stepId));
}
}

View File

@@ -0,0 +1,28 @@
package com.zt.plat.module.databus.enums.gateway;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* External API publish status enumeration.
*/
@AllArgsConstructor
@Getter
public enum ApiStatusEnum {
DRAFT(0),
ONLINE(1),
OFFLINE(2),
DEPRECATED(3);
private final int status;
public static boolean isOnline(Integer status) {
return status != null && status == ONLINE.status;
}
public static boolean isDeprecated(Integer status) {
return status != null && status == DEPRECATED.status;
}
}

View File

@@ -0,0 +1,18 @@
package com.zt.plat.module.databus.enums.gateway;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* Step types supported by the unified API portal.
*/
@AllArgsConstructor
@Getter
public enum ApiStepTypeEnum {
HTTP,
RPC,
SCRIPT,
FLOW;
}

View File

@@ -0,0 +1,32 @@
package com.zt.plat.module.databus.enums.gateway;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Locale;
/**
* Supported expression languages for request/response mapping.
*/
@AllArgsConstructor
@Getter
public enum ExpressionTypeEnum {
JSON("json");
private final String code;
public static ExpressionTypeEnum fromValue(String value) {
if (value == null) {
return null;
}
String normalized = value.trim().toLowerCase(Locale.ROOT);
for (ExpressionTypeEnum type : values()) {
if (type.code.equals(normalized) || type.name().equals(normalized.toUpperCase(Locale.ROOT))) {
return type;
}
}
return null;
}
}

View File

@@ -0,0 +1,28 @@
package com.zt.plat.module.databus.enums.gateway;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* Transformation phase enumeration.
*/
@AllArgsConstructor
@Getter
public enum TransformPhaseEnum {
REQUEST_PRE,
REQUEST_POST,
RESPONSE_PRE,
RESPONSE_POST,
ERROR;
public static TransformPhaseEnum fromCode(String code) {
for (TransformPhaseEnum phase : values()) {
if (phase.name().equalsIgnoreCase(code)) {
return phase;
}
}
return null;
}
}

View File

@@ -0,0 +1,36 @@
package com.zt.plat.module.databus.framework.integration.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.List;
/**
* Configuration properties for the unified API portal.
*/
@Data
@ConfigurationProperties(prefix = "databus.api-portal")
public class ApiGatewayProperties {
private String basePath = "/api/portal";
private List<String> allowedIps = new ArrayList<>();
private List<String> deniedIps = new ArrayList<>();
private boolean enableSignature = false;
private String signatureHeader = "X-Signature";
private String signatureSecret;
private boolean enableTenantHeader = true;
private String tenantHeader = "X-Tenant-Id";
private boolean enableAudit = true;
private boolean enableRateLimit = true;
}

View File

@@ -0,0 +1,25 @@
package com.zt.plat.module.databus.framework.integration.config;
import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum;
import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionEvaluatorRegistry;
import com.zt.plat.module.databus.framework.integration.gateway.expression.JsonataExpressionEvaluator;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
/**
* Registers expression evaluators with the registry.
*/
@Configuration
@RequiredArgsConstructor
public class ExpressionConfiguration {
private final ExpressionEvaluatorRegistry registry;
private final JsonataExpressionEvaluator jsonataExpressionEvaluator;
@PostConstruct
public void registerEvaluators() {
registry.register(ExpressionTypeEnum.JSON, jsonataExpressionEvaluator);
}
}

View File

@@ -0,0 +1,147 @@
package com.zt.plat.module.databus.framework.integration.config;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.module.databus.framework.integration.gateway.core.ApiFlowDispatcher;
import com.zt.plat.module.databus.framework.integration.gateway.core.ApiGatewayRequestMapper;
import com.zt.plat.module.databus.framework.integration.gateway.core.ErrorHandlingStrategy;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import com.zt.plat.module.databus.framework.integration.gateway.security.GatewaySecurityFilter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.http.dsl.Http;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* Configures the unified API portal inbound gateway and supporting beans.
*/
@Slf4j
@Configuration
@EnableConfigurationProperties(ApiGatewayProperties.class)
@RequiredArgsConstructor
public class GatewayIntegrationConfiguration {
private final ApiGatewayProperties properties;
private final ApiGatewayRequestMapper requestMapper;
private final ObjectProvider<ApiFlowDispatcher> apiFlowDispatcherProvider;
private final ErrorHandlingStrategy errorHandlingStrategy;
@Bean(name = "apiPortalTaskExecutor")
public ThreadPoolTaskExecutor apiPortalTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(32);
executor.setQueueCapacity(256);
executor.setThreadNamePrefix("api-portal-");
executor.initialize();
return executor;
}
@Bean
public MessagingTemplate apiPortalMessagingTemplate() {
return new MessagingTemplate();
}
@Bean
public FilterRegistrationBean<GatewaySecurityFilter> gatewaySecurityFilterRegistration(GatewaySecurityFilter filter) {
FilterRegistrationBean<GatewaySecurityFilter> registration = new FilterRegistrationBean<>(filter);
registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 10);
return registration;
}
@Bean
public IntegrationFlow apiGatewayInboundFlow() {
String pattern = properties.getBasePath() + "/{apiCode}/{version}";
return IntegrationFlow.from(Http.inboundGateway(pattern)
.requestMapping(spec -> spec
.methods(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE, HttpMethod.PATCH))
.errorChannel(errorHandlingStrategy.getErrorChannel())
.requestPayloadType(String.class)
.mappedRequestHeaders("*")
.mappedResponseHeaders("*"))
.handle(this, "mapRequest", endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice()))
.handle(this, "dispatch", endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice()))
.handle(this, "buildResponse", endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice()))
.get();
}
public Message<ApiInvocationContext> mapRequest(Message<?> message) {
ApiInvocationContext context = requestMapper.map(message.getPayload(), message.getHeaders());
return MessageBuilder.withPayload(context)
.copyHeaders(message.getHeaders())
.setHeaderIfAbsent("apiCode", context.getApiCode())
.setHeaderIfAbsent("version", context.getApiVersion())
.build();
}
public ApiInvocationContext dispatch(Message<ApiInvocationContext> message) {
ApiInvocationContext context = message.getPayload();
try {
return apiFlowDispatcherProvider.getObject()
.dispatch(context.getApiCode(), context.getApiVersion(), context);
} catch (ServiceException ex) {
handleServiceException(context, ex);
log.warn("[API-PORTAL] ServiceException while dispatching apiCode={} version={}: {}", context.getApiCode(), context.getApiVersion(), ex.getMessage());
return context;
} catch (Exception ex) {
handleUnexpectedException(context, ex);
log.error("[API-PORTAL] Unexpected exception while dispatching apiCode={} version={}", context.getApiCode(), context.getApiVersion(), ex);
return context;
}
}
public ResponseEntity<ApiGatewayResponse> buildResponse(ApiInvocationContext context) {
int status = context.getResponseStatus() != null ? context.getResponseStatus() : HttpStatus.OK.value();
ApiGatewayResponse envelope = ApiGatewayResponse.builder()
.code(status >= 200 && status < 400 ? "SUCCESS" : "ERROR")
.message(StringUtils.hasText(context.getResponseMessage()) ? context.getResponseMessage() : HttpStatus.valueOf(status).getReasonPhrase())
.data(context.getResponseBody())
.traceId(context.getRequestId())
.build();
return ResponseEntity.status(status).body(envelope);
}
private void handleServiceException(ApiInvocationContext context, ServiceException ex) {
String message = StringUtils.hasText(ex.getMessage()) ? ex.getMessage() : "API invocation failed";
context.setResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
context.setResponseMessage(message);
if (context.getResponseBody() == null) {
Map<String, Object> body = new HashMap<>();
if (ex.getCode() != null) {
body.put("errorCode", ex.getCode());
}
body.put("errorMessage", message);
context.setResponseBody(body);
}
}
private void handleUnexpectedException(ApiInvocationContext context, Exception ex) {
String message = StringUtils.hasText(ex.getMessage()) ? ex.getMessage() : "API invocation encountered an unexpected error";
context.setResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
context.setResponseMessage(message);
if (context.getResponseBody() == null) {
Map<String, Object> body = new HashMap<>();
body.put("errorMessage", message);
body.put("exception", ex.getClass().getSimpleName());
context.setResponseBody(body);
}
}
}

View File

@@ -0,0 +1,270 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum;
import com.zt.plat.module.databus.enums.gateway.TransformPhaseEnum;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition;
import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionExecutor;
import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionSpec;
import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionSpecParser;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import com.zt.plat.module.databus.framework.integration.gateway.step.StepHandlerFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.aop.Advice;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.task.TaskExecutor;
import org.springframework.integration.core.GenericHandler;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlowBuilder;
import org.springframework.integration.dsl.MessageChannels;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_PARALLEL_FAILED;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_PARALLEL_INTERRUPTED;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_TRANSFORM_EVALUATION_FAILED;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_TRANSFORM_RESPONSE_STATUS_INVALID;
/**
* Assembles dynamic integration flows per API definition.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ApiFlowAssembler {
private final StepHandlerFactory stepHandlerFactory;
private final PolicyAdvisorFactory policyAdvisorFactory;
private final ErrorHandlingStrategy errorHandlingStrategy;
private final MonitoringInterceptor monitoringInterceptor;
private final ExpressionExecutor expressionExecutor;
@Qualifier("apiPortalTaskExecutor")
private final TaskExecutor apiPortalTaskExecutor;
public ApiFlowRegistration assemble(ApiDefinitionAggregate aggregate) {
String inputChannelName = channelName(aggregate);
String flowId = flowId(aggregate);
IntegrationFlowBuilder builder = IntegrationFlow.from(MessageChannels.direct(inputChannelName)
.datatype(ApiInvocationContext.class)
.interceptor(monitoringInterceptor))
.log(message -> String.format("[API-PORTAL] entering flow %s", flowId))
.handle(ApiInvocationContext.class,
applyTransforms(aggregate, TransformPhaseEnum.REQUEST_PRE),
endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice()));
List<FlowSegment> segments = segments(aggregate.getSteps());
for (FlowSegment segment : segments) {
if (segment instanceof SequentialSegment sequentialSegment) {
builder = applySequential(builder, aggregate, sequentialSegment.getStep());
} else if (segment instanceof ParallelSegment parallelSegment) {
builder = applyParallel(builder, aggregate, parallelSegment);
}
}
builder = builder
.handle(ApiInvocationContext.class,
applyTransforms(aggregate, TransformPhaseEnum.RESPONSE_PRE),
endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice()))
.handle(ApiInvocationContext.class,
(payload, headers) -> payload,
endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice()));
return ApiFlowRegistration.builder()
.flowId(flowId)
.inputChannelName(inputChannelName)
.flow(builder.get())
.build();
}
private GenericHandler<ApiInvocationContext> applyTransforms(ApiDefinitionAggregate aggregate, TransformPhaseEnum phase) {
return (payload, headers) -> {
var transformDefinition = aggregate.getApiLevelTransforms().get(phase.name());
if (transformDefinition != null && StringUtils.hasText(transformDefinition.getExpression())) {
String rawExpression = transformDefinition.getExpressionType() + "::" + transformDefinition.getExpression();
ExpressionSpec spec = ExpressionSpecParser.parse(rawExpression, ExpressionTypeEnum.JSON);
try {
Object result = expressionExecutor.evaluate(spec, payload, payload.getRequestBody(), headers);
applyTransformResult(payload, result);
} catch (Exception ex) {
if (ex instanceof ServiceException serviceException) {
throw serviceException;
}
throw ServiceExceptionUtil.exception(API_TRANSFORM_EVALUATION_FAILED, ex.getMessage());
}
}
return payload;
};
}
private void applyTransformResult(ApiInvocationContext context, Object result) {
if (!(result instanceof Map<?, ?> map)) {
return;
}
Object headerUpdates = map.get("requestHeaders");
if (headerUpdates instanceof Map<?, ?> headerMap) {
headerMap.forEach((key, value) -> context.getRequestHeaders().put(String.valueOf(key), value));
}
Object variableUpdates = map.get("variables");
if (variableUpdates instanceof Map<?, ?> variables) {
variables.forEach((key, value) -> context.getVariables().put(String.valueOf(key), value));
}
Object attributeUpdates = map.get("attributes");
if (attributeUpdates instanceof Map<?, ?> attributes) {
attributes.forEach((key, value) -> context.getAttributes().put(String.valueOf(key), value));
}
if (map.containsKey("responseBody")) {
context.setResponseBody(map.get("responseBody"));
}
if (map.containsKey("responseStatus")) {
context.setResponseStatus(asInteger(map.get("responseStatus")));
}
if (map.containsKey("responseMessage")) {
Object message = map.get("responseMessage");
context.setResponseMessage(message == null ? null : String.valueOf(message));
}
}
private Integer asInteger(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.intValue();
}
try {
return Integer.parseInt(String.valueOf(value));
} catch (NumberFormatException ex) {
throw ServiceExceptionUtil.exception(API_TRANSFORM_RESPONSE_STATUS_INVALID, value);
}
}
private IntegrationFlowBuilder applySequential(IntegrationFlowBuilder builder, ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) {
GenericHandler<ApiInvocationContext> handler = stepHandlerFactory.build(aggregate, stepDefinition);
return builder.handle(ApiInvocationContext.class, handler, endpoint -> {
endpoint.advice(errorHandlingStrategy.errorForwardingAdvice());
Advice[] advices = policyAdvisorFactory.buildAdvices(aggregate, stepDefinition);
if (advices.length > 0) {
endpoint.advice(advices);
}
});
}
private IntegrationFlowBuilder applyParallel(IntegrationFlowBuilder builder, ApiDefinitionAggregate aggregate, ParallelSegment segment) {
return builder.handle(ApiInvocationContext.class,
(payload, headers) -> executeParallel(payload, headers, aggregate, segment),
endpoint -> {
endpoint.advice(errorHandlingStrategy.errorForwardingAdvice());
Advice[] advices = policyAdvisorFactory.buildParallelAdvices(aggregate, segment);
if (advices.length > 0) {
endpoint.advice(advices);
}
});
}
private ApiInvocationContext executeParallel(ApiInvocationContext context, MessageHeaders headers,
ApiDefinitionAggregate aggregate, ParallelSegment segment) {
List<CompletableFuture<ApiInvocationContext>> futures = new ArrayList<>();
for (ApiStepDefinition step : segment.getSteps()) {
GenericHandler<ApiInvocationContext> handler = stepHandlerFactory.build(aggregate, step);
ApiInvocationContext childContext = context.copy();
futures.add(CompletableFuture.supplyAsync(() -> {
handler.handle(childContext, headers);
return childContext;
}, apiPortalTaskExecutor));
}
for (CompletableFuture<ApiInvocationContext> future : futures) {
try {
ApiInvocationContext child = future.get();
context.merge(child);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw ServiceExceptionUtil.exception(API_PARALLEL_INTERRUPTED);
} catch (ExecutionException ex) {
Throwable cause = ex.getCause();
if (cause instanceof ServiceException serviceException) {
throw serviceException;
}
throw ServiceExceptionUtil.exception(API_PARALLEL_FAILED, cause == null ? ex.getMessage() : cause.getMessage());
}
}
return context;
}
private List<FlowSegment> segments(List<ApiStepDefinition> steps) {
return steps.stream()
.sorted(Comparator.comparingInt(step -> step.getStep().getStepOrder() == null ? Integer.MAX_VALUE : step.getStep().getStepOrder()))
.collect(ArrayList::new, this::consumeStep, this::combineSegments);
}
private void consumeStep(List<FlowSegment> segments, ApiStepDefinition step) {
String parallelGroup = step.getStep().getParallelGroup();
if (!StringUtils.hasText(parallelGroup)) {
segments.add(new SequentialSegment(step));
return;
}
FlowSegment last = segments.isEmpty() ? null : segments.get(segments.size() - 1);
if (last instanceof ParallelSegment parallelSegment && parallelGroup.equals(parallelSegment.getGroup())) {
parallelSegment.getSteps().add(step);
} else {
ParallelSegment newSegment = new ParallelSegment(parallelGroup, new ArrayList<>());
newSegment.getSteps().add(step);
segments.add(newSegment);
}
}
private void combineSegments(List<FlowSegment> target, List<FlowSegment> source) {
target.addAll(source);
}
private String channelName(ApiDefinitionAggregate aggregate) {
return "api.portal.flow." + aggregate.getDefinition().getApiCode().toLowerCase() + "." + aggregate.getDefinition().getVersion();
}
private String flowId(ApiDefinitionAggregate aggregate) {
return "apiPortalFlow:" + aggregate.getDefinition().getApiCode() + ":" + aggregate.getDefinition().getVersion();
}
private interface FlowSegment {
}
private static final class SequentialSegment implements FlowSegment {
private final ApiStepDefinition step;
private SequentialSegment(ApiStepDefinition step) {
this.step = step;
}
public ApiStepDefinition getStep() {
return step;
}
}
private static final class ParallelSegment implements FlowSegment {
private final String group;
private final List<ApiStepDefinition> steps;
private ParallelSegment(String group, List<ApiStepDefinition> steps) {
this.group = group;
this.steps = steps;
}
public String getGroup() {
return group;
}
public List<ApiStepDefinition> getSteps() {
return steps;
}
}
}

View File

@@ -0,0 +1,38 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import lombok.RequiredArgsConstructor;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_FLOW_NOT_FOUND;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_FLOW_NO_REPLY;
/**
* Dispatches API invocation contexts to the appropriate integration flow.
*/
@Component
@RequiredArgsConstructor
public class ApiFlowDispatcher {
private final IntegrationFlowManager integrationFlowManager;
private final MessagingTemplate messagingTemplate;
public ApiInvocationContext dispatch(String apiCode, String version, ApiInvocationContext context) {
MessageChannel channel = integrationFlowManager.locateInputChannel(apiCode, version)
.orElseThrow(() -> ServiceExceptionUtil.exception(API_FLOW_NOT_FOUND, apiCode, version));
Message<ApiInvocationContext> message = MessageBuilder.withPayload(context)
.setHeader("apiCode", apiCode)
.setHeader("version", version)
.build();
Message<?> reply = messagingTemplate.sendAndReceive(channel, message);
if (reply == null) {
throw ServiceExceptionUtil.exception(API_FLOW_NO_REPLY, apiCode, version);
}
return (ApiInvocationContext) reply.getPayload();
}
}

View File

@@ -0,0 +1,20 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import lombok.Builder;
import lombok.Value;
import org.springframework.integration.dsl.IntegrationFlow;
/**
* Metadata returned by the assembler for flow registration.
*/
@Value
@Builder
public class ApiFlowRegistration {
String flowId;
String inputChannelName;
IntegrationFlow flow;
}

View File

@@ -0,0 +1,82 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerMapping;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
/**
* Maps inbound HTTP request metadata into {@link ApiInvocationContext} instances.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ApiGatewayRequestMapper {
private final ObjectMapper objectMapper;
private final ApiGatewayProperties properties;
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";
@SuppressWarnings("unchecked")
public ApiInvocationContext map(Object payload, Map<String, Object> headers) {
ApiInvocationContext context = ApiInvocationContext.create();
Map<String, Object> uriVariables = (Map<String, Object>) headers.get(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
if (uriVariables != null) {
context.setApiCode(String.valueOf(uriVariables.get("apiCode")));
context.setApiVersion(String.valueOf(uriVariables.get("version")));
}
Object methodHeader = headers.get(org.springframework.integration.http.HttpHeaders.REQUEST_METHOD);
if (methodHeader != null) {
context.setHttpMethod(String.valueOf(methodHeader));
}
Object requestPath = headers.get(HEADER_REQUEST_URI);
if (requestPath == null) {
requestPath = headers.get(org.springframework.integration.http.HttpHeaders.REQUEST_URL);
}
if (requestPath != null) {
context.setRequestPath(String.valueOf(requestPath));
}
Map<String, Object> requestHeaders = (Map<String, Object>) headers.get(HEADER_REQUEST_HEADERS);
if (requestHeaders != null) {
requestHeaders.forEach((key, value) -> context.getRequestHeaders().put(key, String.valueOf(value)));
}
if (properties.isEnableTenantHeader()) {
Object tenantHeaderValue = context.getRequestHeaders().get(properties.getTenantHeader());
if (tenantHeaderValue != null) {
context.setTenantId(String.valueOf(tenantHeaderValue));
}
}
if (payload instanceof String body) {
if (StringUtils.hasText(body) && isJsonContent(context)) {
try {
context.setRequestBody(objectMapper.readValue(body, Object.class));
} catch (IOException ex) {
log.warn("Failed to parse request body as JSON", ex);
context.setRequestBody(body);
}
} else {
context.setRequestBody(body);
}
} else {
context.setRequestBody(payload);
}
return context;
}
private boolean isJsonContent(ApiInvocationContext context) {
String contentType = String.valueOf(context.getRequestHeaders().getOrDefault(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)).toLowerCase(Locale.ROOT);
return contentType.contains(MediaType.APPLICATION_JSON_VALUE);
}
}

View File

@@ -0,0 +1,73 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.dsl.MessageChannels;
import org.springframework.integration.handler.advice.AbstractHandleMessageAdvice;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.stereotype.Component;
/**
* Centralized error channel and handler for the API portal.
*/
@Slf4j
@Component
public class ErrorHandlingStrategy {
@Getter
private final MessageChannel errorChannel;
private final Advice errorForwardingAdvice;
public ErrorHandlingStrategy() {
DirectChannel channel = MessageChannels.direct("apiPortalErrorChannel").getObject();
this.errorChannel = channel;
channel.subscribe(this::handleErrorMessage);
this.errorForwardingAdvice = new ErrorForwardingAdvice();
}
public Advice errorForwardingAdvice() {
return errorForwardingAdvice;
}
private void handleErrorMessage(Message<?> message) {
if (message instanceof ErrorMessage errorMessage) {
handleError(errorMessage);
}
}
private void handleError(ErrorMessage errorMessage) {
Throwable throwable = errorMessage.getPayload();
Message<?> failedMessage = errorMessage.getOriginalMessage();
if (failedMessage != null && failedMessage.getPayload() instanceof ApiInvocationContext context) {
context.setResponseStatus(500);
context.setResponseMessage(throwable.getMessage());
}
log.error("[API-PORTAL] Integration flow error", throwable);
}
private class ErrorForwardingAdvice extends AbstractHandleMessageAdvice {
@Override
protected Object doInvoke(MethodInvocation invocation, Message<?> message) throws Throwable {
try {
return invocation.proceed();
} catch (Throwable ex) {
ErrorMessage errorMessage = new ErrorMessage(ex, message);
try {
if (!errorChannel.send(errorMessage)) {
log.warn("[API-PORTAL] Failed to forward error message to channel {}", errorChannel);
}
} catch (Exception sendEx) {
log.error("[API-PORTAL] Error while submitting message to error channel", sendEx);
}
throw ex;
}
}
}
}

View File

@@ -0,0 +1,96 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.service.gateway.ApiDefinitionService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.integration.dsl.context.IntegrationFlowContext;
import org.springframework.messaging.MessageChannel;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* Manages dynamic registration of API integration flows.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class IntegrationFlowManager {
private final IntegrationFlowContext integrationFlowContext;
private final ApiDefinitionService apiDefinitionService;
private final ApiFlowAssembler apiFlowAssembler;
private final Map<String, IntegrationFlowContext.IntegrationFlowRegistration> activeRegistrations = new ConcurrentHashMap<>();
@PostConstruct
public void bootstrap() {
refreshAll();
}
public void refreshAll() {
List<ApiDefinitionAggregate> aggregates = apiDefinitionService.loadActiveDefinitions();
Map<String, ApiDefinitionAggregate> desired = new ConcurrentHashMap<>();
for (ApiDefinitionAggregate aggregate : aggregates) {
desired.put(key(aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion()), aggregate);
}
// remove flows that are no longer active
activeRegistrations.keySet().stream()
.filter(existingKey -> !desired.containsKey(existingKey))
.forEach(this::deregisterByKey);
// register or refresh active flows
desired.values().forEach(this::registerFlow);
}
public void refresh(String apiCode, String version) {
apiDefinitionService.refresh(apiCode, version)
.ifPresentOrElse(this::registerFlow, () -> deregister(apiCode, version));
}
public Optional<MessageChannel> locateInputChannel(String apiCode, String version) {
String key = key(apiCode, version);
IntegrationFlowContext.IntegrationFlowRegistration registration = activeRegistrations.get(key);
if (registration == null) {
return Optional.empty();
}
return Optional.ofNullable(registration.getInputChannel());
}
private void registerFlow(ApiDefinitionAggregate aggregate) {
String key = key(aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion());
deregisterByKey(key);
ApiFlowRegistration apiFlowRegistration = apiFlowAssembler.assemble(aggregate);
IntegrationFlowContext.IntegrationFlowRegistration registration = integrationFlowContext.registration(apiFlowRegistration.getFlow())
.id(apiFlowRegistration.getFlowId())
.register();
activeRegistrations.put(key, registration);
log.info("[API-PORTAL] registered flow {} for apiCode={} version={}", apiFlowRegistration.getFlowId(), aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion());
}
private void deregister(String apiCode, String version) {
deregisterByKey(key(apiCode, version));
}
private void deregisterByKey(String key) {
IntegrationFlowContext.IntegrationFlowRegistration existing = activeRegistrations.remove(key);
if (existing != null) {
try {
integrationFlowContext.remove(existing.getId());
log.info("[API-PORTAL] deregistered flow {} for key {}", existing.getId(), key);
} catch (Exception ex) {
log.warn("Failed to remove integration flow {}", existing.getId(), ex);
}
}
}
private String key(String apiCode, String version) {
return (apiCode + ":" + version).toLowerCase();
}
}

View File

@@ -0,0 +1,54 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
/**
* Channel interceptor capturing timing metrics and enriched logging.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class MonitoringInterceptor implements ChannelInterceptor {
private static final String HEADER_START_TIME = "ApiPortalStartTime";
private final MeterRegistry meterRegistry;
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
return MessageBuilder.fromMessage(message)
.setHeader(HEADER_START_TIME, Instant.now())
.build();
}
@Override
public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, Exception ex) {
Instant start = message.getHeaders().get(HEADER_START_TIME, Instant.class);
if (start != null) {
Duration duration = Duration.between(start, Instant.now());
Object payload = message.getPayload();
if (payload instanceof ApiInvocationContext context) {
Timer.builder("api.portal.latency")
.tag("api", context.getApiCode())
.tag("version", context.getApiVersion())
.register(meterRegistry)
.record(duration);
}
}
if (ex != null) {
log.error("[API-PORTAL] Channel send failed", ex);
}
}
}

View File

@@ -0,0 +1,166 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import com.zt.plat.module.databus.framework.integration.gateway.policy.AuthPolicyEvaluator;
import com.zt.plat.module.databus.framework.integration.gateway.policy.RateLimitPolicyEvaluator;
import lombok.RequiredArgsConstructor;
import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice;
import org.springframework.integration.handler.advice.RequestHandlerRetryAdvice;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_EXECUTION_ERROR;
/**
* Builds advice chains for steps based on configured policies.
*/
@Component
@RequiredArgsConstructor
public class PolicyAdvisorFactory {
private final AuthPolicyEvaluator authPolicyEvaluator;
private final RateLimitPolicyEvaluator rateLimitPolicyEvaluator;
public org.aopalliance.aop.Advice[] buildAdvices(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) {
List<org.aopalliance.aop.Advice> advices = new ArrayList<>();
advices.add(new AuthPolicyAdvice(aggregate));
advices.add(new RateLimitPolicyAdvice(aggregate));
advices.add(createRetryAdvice(stepDefinition));
return advices.stream().filter(advice -> advice != null).toArray(org.aopalliance.aop.Advice[]::new);
}
public org.aopalliance.aop.Advice[] buildParallelAdvices(ApiDefinitionAggregate aggregate, Object segment) {
// For parallel segments we reuse the same advice chain (auth + rateLimit once at entry)
return buildAdvices(aggregate, null);
}
private RequestHandlerRetryAdvice createRetryAdvice(ApiStepDefinition stepDefinition) {
if (stepDefinition == null) {
return null;
}
Object strategyConfig = stepDefinition.getMetadata().get("retryStrategy");
if (!(strategyConfig instanceof Map<?, ?> configMap)) {
return null;
}
RetryTemplate template = new RetryTemplate();
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
int maxAttempts = asInt(configMap.get("maxAttempts"), 3);
retryPolicy.setMaxAttempts(maxAttempts);
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
long initialInterval = asLong(configMap.get("initialInterval"), 200L);
double multiplier = asDouble(configMap.get("multiplier"), 2.0d);
long maxInterval = asLong(configMap.get("maxInterval"), 2000L);
backOffPolicy.setInitialInterval(initialInterval);
backOffPolicy.setMultiplier(multiplier);
backOffPolicy.setMaxInterval(maxInterval);
template.setBackOffPolicy(backOffPolicy);
template.setRetryPolicy(retryPolicy);
RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice();
advice.setRetryTemplate(template);
return advice;
}
private final class AuthPolicyAdvice extends AbstractRequestHandlerAdvice {
private final ApiDefinitionAggregate aggregate;
private AuthPolicyAdvice(ApiDefinitionAggregate aggregate) {
this.aggregate = aggregate;
}
@Override
protected Object doInvoke(ExecutionCallback callback, Object target, org.springframework.messaging.Message<?> message) {
if (aggregate.getAuthPolicy() != null) {
authPolicyEvaluator.evaluate(aggregate, (ApiInvocationContext) message.getPayload());
}
try {
return callback.execute();
} catch (Exception ex) {
if (ex instanceof ServiceException serviceException) {
throw serviceException;
}
if (ex instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw ServiceExceptionUtil.exception(API_STEP_EXECUTION_ERROR, ex.getMessage());
}
}
}
private final class RateLimitPolicyAdvice extends AbstractRequestHandlerAdvice {
private final ApiDefinitionAggregate aggregate;
private RateLimitPolicyAdvice(ApiDefinitionAggregate aggregate) {
this.aggregate = aggregate;
}
@Override
protected Object doInvoke(ExecutionCallback callback, Object target, org.springframework.messaging.Message<?> message) {
if (aggregate.getRateLimitPolicy() != null) {
rateLimitPolicyEvaluator.evaluate(aggregate, (ApiInvocationContext) message.getPayload());
}
try {
return callback.execute();
} catch (Exception ex) {
if (ex instanceof ServiceException serviceException) {
throw serviceException;
}
if (ex instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw ServiceExceptionUtil.exception(API_STEP_EXECUTION_ERROR, ex.getMessage());
}
}
}
private int asInt(Object value, int defaultValue) {
if (value instanceof Number number) {
return number.intValue();
}
if (value instanceof String text) {
try {
return Integer.parseInt(text);
} catch (NumberFormatException ignored) {
// ignore and fall back to default
}
}
return defaultValue;
}
private long asLong(Object value, long defaultValue) {
if (value instanceof Number number) {
return number.longValue();
}
if (value instanceof String text) {
try {
return Long.parseLong(text);
} catch (NumberFormatException ignored) {
// ignore and fall back to default
}
}
return defaultValue;
}
private double asDouble(Object value, double defaultValue) {
if (value instanceof Number number) {
return number.doubleValue();
}
if (value instanceof String text) {
try {
return Double.parseDouble(text);
} catch (NumberFormatException ignored) {
// ignore and fall back to default
}
}
return defaultValue;
}
}

View File

@@ -0,0 +1,40 @@
package com.zt.plat.module.databus.framework.integration.gateway.domain;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO;
import lombok.Builder;
import lombok.Value;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Aggregate representing an API definition with its steps and policies.
*/
@Value
@Builder(toBuilder = true)
public class ApiDefinitionAggregate {
ApiDefinitionDO definition;
List<ApiStepDefinition> steps;
Map<String, ApiTransformDefinition> apiLevelTransforms;
ApiPolicyAuthDO authPolicy;
ApiPolicyRateLimitDO rateLimitPolicy;
ApiFlowPublication publication;
public List<ApiStepDefinition> getSteps() {
return steps == null ? Collections.emptyList() : steps;
}
public Map<String, ApiTransformDefinition> getApiLevelTransforms() {
return apiLevelTransforms == null ? Collections.emptyMap() : apiLevelTransforms;
}
}

View File

@@ -0,0 +1,25 @@
package com.zt.plat.module.databus.framework.integration.gateway.domain;
import lombok.Builder;
import lombok.Value;
/**
* Publication metadata for an API flow.
*/
@Value
@Builder(toBuilder = true)
public class ApiFlowPublication {
Long id;
String releaseTag;
String snapshot;
String status;
boolean active;
String description;
}

View File

@@ -0,0 +1,32 @@
package com.zt.plat.module.databus.framework.integration.gateway.domain;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiStepDO;
import lombok.Builder;
import lombok.Value;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Domain representation of an orchestration step.
*/
@Value
@Builder(toBuilder = true)
public class ApiStepDefinition {
ApiStepDO step;
List<ApiTransformDefinition> transforms;
Map<String, Object> metadata;
public List<ApiTransformDefinition> getTransforms() {
return transforms == null ? Collections.emptyList() : transforms;
}
public Map<String, Object> getMetadata() {
return metadata == null ? Collections.emptyMap() : metadata;
}
}

View File

@@ -0,0 +1,23 @@
package com.zt.plat.module.databus.framework.integration.gateway.domain;
import lombok.Builder;
import lombok.Value;
/**
* Domain representation for transformation expression metadata.
*/
@Value
@Builder(toBuilder = true)
public class ApiTransformDefinition {
Long id;
String phase;
String expressionType;
String expression;
String description;
}

View File

@@ -0,0 +1,24 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import lombok.Builder;
import lombok.Value;
import java.util.Map;
/**
* Context provided to expression engines when evaluating mappings.
*/
@Value
@Builder
public class ExpressionEvaluationContext {
ApiInvocationContext invocation;
Object payload;
Map<String, Object> variables;
Map<String, Object> headers;
}

View File

@@ -0,0 +1,10 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
/**
* Expression evaluator contract.
*/
public interface ExpressionEvaluator {
Object evaluate(String expression, ExpressionEvaluationContext context) throws Exception;
}

View File

@@ -0,0 +1,28 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
/**
* Registry maintaining expression evaluators per language.
*/
@Component
@RequiredArgsConstructor
public class ExpressionEvaluatorRegistry {
private final Map<ExpressionTypeEnum, ExpressionEvaluator> evaluators = new EnumMap<>(ExpressionTypeEnum.class);
public void register(ExpressionTypeEnum type, ExpressionEvaluator evaluator) {
evaluators.put(type, evaluator);
}
public Optional<ExpressionEvaluator> lookup(ExpressionTypeEnum type) {
return Optional.ofNullable(evaluators.get(type));
}
}

View File

@@ -0,0 +1,51 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Optional;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_EXPRESSION_EVALUATION_FAILED;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_EXPRESSION_NO_EVALUATOR;
/**
* Executes expressions using registered evaluators.
*/
@Component
@RequiredArgsConstructor
public class ExpressionExecutor {
private final ExpressionEvaluatorRegistry registry;
public Object evaluate(ExpressionSpec spec, ApiInvocationContext invocation, Object payload, Map<String, Object> headers) throws Exception {
if (spec == null || spec.getExpression() == null) {
return null;
}
ExpressionTypeEnum type = spec.getType();
return registry.lookup(type)
.orElseThrow(() -> ServiceExceptionUtil.exception(API_EXPRESSION_NO_EVALUATOR, type == null ? "" : type.name()))
.evaluate(spec.getExpression(), ExpressionEvaluationContext.builder()
.invocation(invocation)
.payload(payload)
.variables(invocation.getVariables())
.headers(headers)
.build());
}
public Optional<Object> evaluateOptional(ExpressionSpec spec, ApiInvocationContext invocation, Object payload, Map<String, Object> headers) {
try {
return Optional.ofNullable(evaluate(spec, invocation, payload, headers));
} catch (Exception ex) {
if (ex instanceof ServiceException serviceException) {
throw serviceException;
}
throw ServiceExceptionUtil.exception(API_EXPRESSION_EVALUATION_FAILED, ex.getMessage());
}
}
}

View File

@@ -0,0 +1,21 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum;
import lombok.Builder;
import lombok.Value;
/**
* Parsed expression specification with language metadata.
*/
@Value
@Builder
public class ExpressionSpec {
ExpressionTypeEnum type;
String expression;
public static ExpressionSpec of(ExpressionTypeEnum type, String expression) {
return ExpressionSpec.builder().type(type).expression(expression).build();
}
}

View File

@@ -0,0 +1,32 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum;
import org.springframework.util.StringUtils;
/**
* Helper to parse unified expression definitions.
*/
public final class ExpressionSpecParser {
private ExpressionSpecParser() {
}
public static ExpressionSpec parse(String rawExpression, ExpressionTypeEnum defaultType) {
if (!StringUtils.hasText(rawExpression)) {
return null;
}
String trimmed = rawExpression.trim();
int separator = trimmed.indexOf("::");
if (separator < 0) {
return ExpressionSpec.of(defaultType, trimmed);
}
String type = trimmed.substring(0, separator);
String expression = trimmed.substring(separator + 2);
ExpressionTypeEnum expressionTypeEnum = ExpressionTypeEnum.fromValue(type);
if (expressionTypeEnum == null) {
expressionTypeEnum = defaultType;
}
return ExpressionSpec.of(expressionTypeEnum, expression);
}
}

View File

@@ -0,0 +1,13 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
/**
* Legacy placeholder kept for binary compatibility. JSR-223 scripts are no longer supported.
*/
@Deprecated(forRemoval = true)
public class JsScriptExpressionEvaluator implements ExpressionEvaluator {
@Override
public Object evaluate(String expression, ExpressionEvaluationContext context) {
throw new UnsupportedOperationException("JSR-223 script expressions are no longer supported. Use JSON expressions instead.");
}
}

View File

@@ -0,0 +1,50 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
import com.api.jsonata4java.expressions.*;
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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.IOException;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_JSONATA_BIND_FAILED;
/**
* JSONata expression evaluator for JSON payload transformation.
* @author chenbowen
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JsonataExpressionEvaluator implements ExpressionEvaluator {
private final ObjectMapper objectMapper;
@Override
public Object evaluate(String expression, ExpressionEvaluationContext context) throws ParseException, EvaluateException, JsonProcessingException, IOException {
Expressions expressions = Expressions.parse(expression);
bindEnvironment(expressions.getEnvironment(), context);
JsonNode payloadNode = objectMapper.valueToTree(context.getPayload());
JsonNode resultNode = expressions.evaluate(payloadNode);
if (resultNode == null || resultNode.isNull()) {
return null;
}
return objectMapper.treeToValue(resultNode, Object.class);
}
private void bindEnvironment(FrameEnvironment environment, ExpressionEvaluationContext context) {
if (environment == null) {
return;
}
try {
environment.setVariable("vars", objectMapper.valueToTree(context.getVariables() == null ? java.util.Collections.emptyMap() : context.getVariables()));
environment.setVariable("headers", objectMapper.valueToTree(context.getHeaders() == null ? java.util.Collections.emptyMap() : context.getHeaders()));
environment.setVariable("ctx", objectMapper.valueToTree(context.getInvocation()));
} catch (EvaluateRuntimeException e) {
throw ServiceExceptionUtil.exception(API_JSONATA_BIND_FAILED);
}
}
}

View File

@@ -0,0 +1,13 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
/**
* Legacy placeholder kept for binary compatibility. MVEL evaluation is no longer supported.
*/
@Deprecated(forRemoval = true)
public class MvelExpressionEvaluator implements ExpressionEvaluator {
@Override
public Object evaluate(String expression, ExpressionEvaluationContext context) {
throw new UnsupportedOperationException("MVEL expressions are no longer supported. Use JSON expressions instead.");
}
}

View File

@@ -0,0 +1,13 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
/**
* Legacy placeholder kept for binary compatibility. SpEL evaluation is no longer supported.
*/
@Deprecated(forRemoval = true)
public class SpelExpressionEvaluator implements ExpressionEvaluator {
@Override
public Object evaluate(String expression, ExpressionEvaluationContext context) {
throw new UnsupportedOperationException("SpEL expressions are no longer supported. Use JSON expressions instead.");
}
}

View File

@@ -0,0 +1,19 @@
package com.zt.plat.module.databus.framework.integration.gateway.init;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
/**
* Applies idempotent data adjustments required for gateway orchestration features
* before integration flows bootstrap.
*/
@Slf4j
@Component("gatewayPolicyMigration")
public class GatewayPolicyMigration {
@PostConstruct
public void migrate() {
log.info("[API-PORTAL] gateway policy migration skipped; standard header token auth in use");
}
}

View File

@@ -0,0 +1,21 @@
package com.zt.plat.module.databus.framework.integration.gateway.model;
import lombok.Builder;
import lombok.Value;
/**
* Standardized response wrapper returned to external clients.
*/
@Value
@Builder
public class ApiGatewayResponse {
String code;
String message;
Object data;
String traceId;
}

View File

@@ -0,0 +1,115 @@
package com.zt.plat.module.databus.framework.integration.gateway.model;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Runtime context for an API invocation flowing through the integration pipeline.
*/
@Getter
@Setter
@ToString
public class ApiInvocationContext {
private final String requestId;
private final Instant requestTime;
private String apiCode;
private String apiVersion;
private String tenantId;
private String httpMethod;
private String requestPath;
private Map<String, Object> requestHeaders;
private Object requestBody;
private Map<String, Object> requestQueryParams;
private Map<String, Object> variables;
private Map<String, Object> attributes;
private List<ApiStepResult> stepResults;
private Object responseBody;
private Integer responseStatus;
private String responseMessage;
public ApiInvocationContext() {
this.requestId = UUID.randomUUID().toString();
this.requestTime = Instant.now();
this.variables = new HashMap<>();
this.attributes = new HashMap<>();
this.stepResults = new ArrayList<>();
this.requestHeaders = new HashMap<>();
this.requestQueryParams = new HashMap<>();
}
public static ApiInvocationContext create() {
return new ApiInvocationContext();
}
public ApiInvocationContext copy() {
ApiInvocationContext copy = new ApiInvocationContext();
copy.apiCode = this.apiCode;
copy.apiVersion = this.apiVersion;
copy.tenantId = this.tenantId;
copy.httpMethod = this.httpMethod;
copy.requestPath = this.requestPath;
copy.requestBody = this.requestBody;
copy.requestQueryParams.putAll(this.requestQueryParams);
copy.responseBody = this.responseBody;
copy.responseStatus = this.responseStatus;
copy.responseMessage = this.responseMessage;
copy.getRequestHeaders().putAll(this.requestHeaders);
copy.getVariables().putAll(this.variables);
copy.getAttributes().putAll(this.attributes);
return copy;
}
public void addStepResult(ApiStepResult result) {
this.stepResults.add(result);
}
public ApiStepResult lastStepResult() {
if (stepResults.isEmpty()) {
return null;
}
return stepResults.get(stepResults.size() - 1);
}
public void merge(ApiInvocationContext other) {
if (other == null) {
return;
}
this.stepResults.addAll(other.getStepResults());
this.variables.putAll(other.getVariables());
this.attributes.putAll(other.getAttributes());
if (other.getResponseBody() != null) {
this.responseBody = other.getResponseBody();
}
if (other.getResponseStatus() != null) {
this.responseStatus = other.getResponseStatus();
}
if (other.getResponseMessage() != null) {
this.responseMessage = other.getResponseMessage();
}
}
}

View File

@@ -0,0 +1,35 @@
package com.zt.plat.module.databus.framework.integration.gateway.model;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.time.Duration;
/**
* Result of executing a single orchestration step.
*/
@Getter
@Setter
@ToString
@Builder(toBuilder = true)
public class ApiStepResult {
private Long stepId;
private String stepType;
private Object request;
private Object response;
private boolean success;
private Duration elapsed;
private String errorCode;
private String errorMessage;
}

View File

@@ -0,0 +1,13 @@
package com.zt.plat.module.databus.framework.integration.gateway.policy;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
/**
* Performs authentication / authorization policy evaluation for a request.
*/
public interface AuthPolicyEvaluator {
void evaluate(ApiDefinitionAggregate aggregate, ApiInvocationContext context);
}

View File

@@ -0,0 +1,56 @@
package com.zt.plat.module.databus.framework.integration.gateway.policy;
import com.zt.plat.framework.common.biz.system.oauth2.OAuth2TokenCommonApi;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_AUTH_UNAUTHORIZED;
/**
* Basic authentication evaluator delegating token validation to system module.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DefaultAuthPolicyEvaluator implements AuthPolicyEvaluator {
private static final String TOKEN_HEADER = "ZT-Auth-Token";
private final OAuth2TokenCommonApi oauth2TokenCommonApi;
@Override
public void evaluate(ApiDefinitionAggregate aggregate, ApiInvocationContext context) {
ApiPolicyAuthDO authPolicy = aggregate.getAuthPolicy();
if (authPolicy == null) {
return;
}
validateHeaderToken(context);
}
private void validateHeaderToken(ApiInvocationContext context) {
Object rawHeader = context.getRequestHeaders().get(TOKEN_HEADER);
String token = rawHeader == null ? null : String.valueOf(rawHeader).trim();
if (!StringUtils.hasText(token)) {
throw ServiceExceptionUtil.exception(API_AUTH_UNAUTHORIZED);
}
try {
oauth2TokenCommonApi.checkAccessToken(token).getCheckedData();
context.getAttributes().putIfAbsent("accessToken", token);
String bearerToken = token.startsWith("Bearer ") ? token : "Bearer " + token;
context.getRequestHeaders().putIfAbsent("Authorization", bearerToken);
} catch (ServiceException ex) {
log.warn("Access token validation failed: {}", ex.getMessage());
throw ServiceExceptionUtil.exception(API_AUTH_UNAUTHORIZED);
} catch (RuntimeException ex) {
log.error("Access token validation error", ex);
throw ServiceExceptionUtil.exception(API_AUTH_UNAUTHORIZED);
}
}
}

View File

@@ -0,0 +1,61 @@
package com.zt.plat.module.databus.framework.integration.gateway.policy;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_RATE_LIMIT_EVALUATION_FAILED;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_RATE_LIMIT_EXCEEDED;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Simple Redis-backed rate limit evaluator supporting fixed window counters.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DefaultRateLimitPolicyEvaluator implements RateLimitPolicyEvaluator {
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {
};
private final ObjectMapper objectMapper;
private final StringRedisTemplate stringRedisTemplate;
@Override
public void evaluate(ApiDefinitionAggregate aggregate, ApiInvocationContext context) {
ApiPolicyRateLimitDO rateLimitDO = aggregate.getRateLimitPolicy();
if (rateLimitDO == null || !StringUtils.hasText(rateLimitDO.getConfig())) {
return;
}
try {
Map<String, Object> config = objectMapper.readValue(rateLimitDO.getConfig(), MAP_TYPE);
long limit = ((Number) config.getOrDefault("limit", 100)).longValue();
long windowSeconds = ((Number) config.getOrDefault("windowSeconds", 60)).longValue();
String key = String.format("databus:api:rl:%s:%s:%s", aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion(), context.getRequestHeaders().getOrDefault("X-Client-Id", "anonymous"));
Long counter = stringRedisTemplate.opsForValue().increment(key);
if (counter != null && counter == 1L) {
stringRedisTemplate.expire(key, Duration.ofSeconds(windowSeconds));
}
if (counter != null && counter > limit) {
throw ServiceExceptionUtil.exception(API_RATE_LIMIT_EXCEEDED);
}
} catch (JsonProcessingException | DataAccessException ex) {
log.error("Rate limit evaluation failed for api {}", aggregate.getDefinition().getApiCode(), ex);
throw ServiceExceptionUtil.exception(API_RATE_LIMIT_EVALUATION_FAILED);
}
}
}

View File

@@ -0,0 +1,13 @@
package com.zt.plat.module.databus.framework.integration.gateway.policy;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
/**
* Applies rate limiting decisions for the invocation.
*/
public interface RateLimitPolicyEvaluator {
void evaluate(ApiDefinitionAggregate aggregate, ApiInvocationContext context);
}

View File

@@ -0,0 +1,75 @@
package com.zt.plat.module.databus.framework.integration.gateway.security;
import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.HmacUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
/**
* Security filter performing IP allow/deny, signature validation, and tenant extraction for the unified portal.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class GatewaySecurityFilter extends OncePerRequestFilter {
private final ApiGatewayProperties properties;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String requestPath = request.getRequestURI();
if (!pathMatcher.match(properties.getBasePath() + "/**", requestPath)) {
filterChain.doFilter(request, response);
return;
}
if (!isIpAllowed(request)) {
response.sendError(HttpStatus.FORBIDDEN.value(), "IP not allowed");
return;
}
if (properties.isEnableSignature() && !validateSignature(request)) {
response.sendError(HttpStatus.UNAUTHORIZED.value(), "Invalid signature");
return;
}
filterChain.doFilter(request, response);
}
private boolean isIpAllowed(HttpServletRequest request) {
String remoteIp = request.getRemoteAddr();
List<String> denied = properties.getDeniedIps();
if (!CollectionUtils.isEmpty(denied) && denied.contains(remoteIp)) {
return false;
}
List<String> allowed = properties.getAllowedIps();
return CollectionUtils.isEmpty(allowed) || allowed.contains(remoteIp);
}
private boolean validateSignature(HttpServletRequest request) {
String headerSignature = request.getHeader(properties.getSignatureHeader());
if (!StringUtils.hasText(headerSignature)) {
return false;
}
String secret = properties.getSignatureSecret();
if (!StringUtils.hasText(secret)) {
log.warn("Signature verification enabled but no secret configured");
return false;
}
String payload = request.getRequestURI() + "|" + (request.getQueryString() == null ? "" : request.getQueryString());
String computed = HmacUtils.hmacSha256Hex(secret, payload);
return headerSignature.equalsIgnoreCase(computed);
}
}

View File

@@ -0,0 +1,17 @@
package com.zt.plat.module.databus.framework.integration.gateway.step;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import org.springframework.integration.core.GenericHandler;
/**
* Contract for building a Spring Integration handler for a specific step type.
*/
public interface ApiStepHandler {
boolean supports(String stepType);
GenericHandler<ApiInvocationContext> build(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition);
}

View File

@@ -0,0 +1,34 @@
package com.zt.plat.module.databus.framework.integration.gateway.step;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.enums.gateway.ApiStepTypeEnum;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import lombok.RequiredArgsConstructor;
import org.springframework.integration.core.GenericHandler;
import org.springframework.stereotype.Component;
import java.util.List;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_UNSUPPORTED_TYPE;
/**
* Delegates step handler creation to registered implementations.
*/
@Component
@RequiredArgsConstructor
public class StepHandlerFactory {
private final List<ApiStepHandler> stepHandlers;
public GenericHandler<ApiInvocationContext> build(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) {
ApiStepTypeEnum type = ApiStepTypeEnum.valueOf(stepDefinition.getStep().getType().toUpperCase());
return stepHandlers.stream()
.filter(handler -> handler.supports(type.name()))
.findFirst()
.orElseThrow(() -> ServiceExceptionUtil.exception(API_STEP_UNSUPPORTED_TYPE, type.name()))
.build(aggregate, stepDefinition);
}
}

View File

@@ -0,0 +1,395 @@
package com.zt.plat.module.databus.framework.integration.gateway.step.impl;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition;
import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionExecutor;
import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionSpec;
import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionSpecParser;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiStepResult;
import com.zt.plat.module.databus.framework.integration.gateway.step.ApiStepHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.integration.core.GenericHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_HTTP_ENDPOINT_INVALID;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_HTTP_EXECUTION_FAILED;
/**
* Step handler that performs outbound HTTP calls.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class HttpStepHandler implements ApiStepHandler {
private final WebClient.Builder webClientBuilder;
private final ExpressionExecutor expressionExecutor;
private static final Set<String> DEFAULT_FORWARDED_HEADERS = Set.of(
"authorization",
"zt-auth-token",
"tenant-id",
"visit-tenant-id",
"visit-company-id",
"visit-company-name",
"visit-dept-id",
"visit-dept-name"
);
@Override
public boolean supports(String stepType) {
return "HTTP".equalsIgnoreCase(stepType);
}
private HttpRequestPayload coerceRequestPayload(Object evaluated, Object fallbackBody, Map<String, Object> fallbackQuery) {
Map<String, Object> querySnapshot = new LinkedHashMap<>(fallbackQuery);
if (evaluated == null) {
return HttpRequestPayload.of(fallbackBody, querySnapshot);
}
if (evaluated instanceof HttpRequestPayload payload) {
Map<String, Object> mergedQuery = new LinkedHashMap<>(fallbackQuery);
mergedQuery.putAll(payload.queryParams());
return HttpRequestPayload.of(payload.body(), mergedQuery);
}
if (evaluated instanceof MultiValueMap<?, ?> multiValueMap) {
mergeQueryParams(querySnapshot, multiValueMap);
return HttpRequestPayload.of(fallbackBody, querySnapshot);
}
if (evaluated instanceof Map<?, ?> map) {
Object queryPart = extractCaseInsensitive(map, "query", "queryParams", "params");
if (queryPart != null) {
mergeQueryParams(querySnapshot, queryPart);
}
boolean explicitBody = containsKeyIgnoreCase(map, "body", "payload");
Object body = explicitBody
? Optional.ofNullable(extractCaseInsensitive(map, "body", "payload")).orElse(fallbackBody)
: (queryPart != null ? fallbackBody : evaluated);
if (!explicitBody && queryPart == null) {
return HttpRequestPayload.of(evaluated, querySnapshot);
}
return HttpRequestPayload.of(body, querySnapshot);
}
return HttpRequestPayload.of(evaluated, querySnapshot);
}
@Override
public GenericHandler<ApiInvocationContext> build(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) {
return (payload, headers) -> {
Instant start = Instant.now();
HttpRequestPayload requestPayload = null;
boolean supportsBody = false;
try {
HttpCallSpec callSpec = parseEndpoint(stepDefinition.getStep().getTargetEndpoint());
supportsBody = supportsRequestBody(callSpec.method);
requestPayload = mapRequest(stepDefinition, payload, headers);
if (!supportsBody && requestPayload != null && requestPayload.body() != null) {
requestPayload = HttpRequestPayload.of(null, requestPayload.queryParams());
}
Map<String, String> headerMap = resolveHeaders(stepDefinition, payload);
Duration timeout = resolveTimeout(stepDefinition);
WebClient client = webClientBuilder.build();
WebClient.RequestHeadersSpec<?> requestSpec = buildRequest(client, callSpec, requestPayload, headerMap, supportsBody);
Mono<Object> responseMono = requestSpec.retrieve().bodyToMono(Object.class);
Object response = timeout == null ? responseMono.block() : responseMono.block(timeout);
payload.addStepResult(ApiStepResult.builder()
.stepId(stepDefinition.getStep().getId())
.stepType(stepDefinition.getStep().getType())
.request(requestPayload == null ? null : requestPayload.snapshot(supportsBody))
.response(response)
.success(true)
.elapsed(Duration.between(start, Instant.now()))
.build());
applyResponseMapping(stepDefinition, payload, headers, response);
} catch (Exception ex) {
payload.addStepResult(ApiStepResult.builder()
.stepId(stepDefinition.getStep().getId())
.stepType(stepDefinition.getStep().getType())
.request(requestPayload == null ? null : requestPayload.snapshot(supportsBody))
.success(false)
.errorMessage(ex.getMessage())
.elapsed(Duration.between(start, Instant.now()))
.build());
if (ex instanceof ServiceException serviceException) {
throw serviceException;
}
throw ServiceExceptionUtil.exception(API_STEP_HTTP_EXECUTION_FAILED, ex.getMessage());
}
return payload;
};
}
private HttpRequestPayload mapRequest(ApiStepDefinition stepDefinition, ApiInvocationContext context, Map<String, Object> headers) throws Exception {
ExpressionSpec spec = ExpressionSpecParser.parse(stepDefinition.getStep().getRequestMappingExpr(), ExpressionTypeEnum.JSON);
Map<String, Object> baseQuery = new LinkedHashMap<>(context.getRequestQueryParams());
Object fallbackBody = context.getRequestBody();
if (spec == null) {
return HttpRequestPayload.of(fallbackBody, baseQuery);
}
Object evaluated = expressionExecutor.evaluate(spec, context, fallbackBody, headers);
return coerceRequestPayload(evaluated, fallbackBody, baseQuery);
}
private void applyResponseMapping(ApiStepDefinition stepDefinition, ApiInvocationContext context, Map<String, Object> headers, Object response) throws Exception {
ExpressionSpec spec = ExpressionSpecParser.parse(stepDefinition.getStep().getResponseMappingExpr(), ExpressionTypeEnum.JSON);
if (spec == null) {
context.setResponseBody(response);
return;
}
Object mapped = expressionExecutor.evaluate(spec, context, response, headers);
if (mapped instanceof Map<?, ?> map) {
map.forEach((key, value) -> context.getVariables().put(String.valueOf(key), value));
} else {
context.setResponseBody(mapped);
}
}
private Map<String, String> resolveHeaders(ApiStepDefinition stepDefinition, ApiInvocationContext context) throws Exception {
Map<String, String> resolved = new LinkedHashMap<>();
context.getRequestHeaders().forEach((key, value) -> {
if (shouldForwardHeader(key) && value != null) {
resolved.put(key, String.valueOf(value));
}
});
Map<String, String> configured = extractConfiguredHeaders(stepDefinition, context);
resolved.putAll(configured);
return resolved;
}
private Map<String, String> extractConfiguredHeaders(ApiStepDefinition stepDefinition, ApiInvocationContext context) throws Exception {
Object headerConfig = stepDefinition.getMetadata().getOrDefault("headers", Collections.emptyMap());
if (headerConfig instanceof Map<?, ?> map) {
return toStringMap(map);
}
ExpressionSpec spec = ExpressionSpecParser.parse((String) stepDefinition.getMetadata().get("headerExpr"), ExpressionTypeEnum.JSON);
if (spec == null) {
return Collections.emptyMap();
}
Object evaluated = expressionExecutor.evaluate(spec, context, context.getRequestBody(), Collections.emptyMap());
if (evaluated instanceof Map<?, ?> map) {
return toStringMap(map);
}
return Collections.emptyMap();
}
private boolean shouldForwardHeader(String headerName) {
if (!StringUtils.hasText(headerName)) {
return false;
}
return DEFAULT_FORWARDED_HEADERS.contains(headerName.toLowerCase(Locale.ROOT));
}
private Map<String, String> toStringMap(Map<?, ?> map) {
if (map == null) {
return Collections.emptyMap();
}
Map<String, String> result = new java.util.LinkedHashMap<>();
map.forEach((key, value) -> {
if (value != null) {
result.put(String.valueOf(key), String.valueOf(value));
}
});
return result;
}
private Duration resolveTimeout(ApiStepDefinition stepDefinition) {
Long timeout = stepDefinition.getStep().getTimeout();
if (timeout == null || timeout <= 0) {
return Duration.ofSeconds(5);
}
return Duration.ofMillis(timeout);
}
private HttpCallSpec parseEndpoint(String targetEndpoint) {
if (!StringUtils.hasText(targetEndpoint)) {
throw ServiceExceptionUtil.exception(API_STEP_HTTP_ENDPOINT_INVALID);
}
String trimmed = targetEndpoint.trim();
String method = "POST";
String url = trimmed;
int spaceIndex = trimmed.indexOf(' ');
if (spaceIndex > 0) {
method = trimmed.substring(0, spaceIndex).toUpperCase();
url = trimmed.substring(spaceIndex + 1);
}
return new HttpCallSpec(HttpMethod.valueOf(method), url);
}
private record HttpCallSpec(HttpMethod method, String url) {
}
private void applyQueryParams(UriComponentsBuilder builder, Map<String, Object> queryParams) {
if (queryParams == null || queryParams.isEmpty()) {
return;
}
queryParams.forEach((key, value) -> addQueryParam(builder, key, value));
}
private void addQueryParam(UriComponentsBuilder builder, String key, Object value) {
if (!StringUtils.hasText(key)) {
return;
}
if (value == null) {
builder.queryParam(key);
return;
}
if (value instanceof MultiValueMap<?, ?> multiValueMap) {
multiValueMap.forEach((innerKey, values) -> addQueryParam(builder, String.valueOf(innerKey), values));
return;
}
if (value instanceof Iterable<?> iterable) {
iterable.forEach(item -> addQueryParam(builder, key, item));
return;
}
if (value.getClass().isArray()) {
int length = java.lang.reflect.Array.getLength(value);
for (int i = 0; i < length; i++) {
addQueryParam(builder, key, java.lang.reflect.Array.get(value, i));
}
return;
}
builder.queryParam(key, value);
}
private void mergeQueryParams(Map<String, Object> target, Object addition) {
if (addition == null || target == null) {
return;
}
if (addition instanceof MultiValueMap<?, ?> multiValueMap) {
multiValueMap.forEach((key, values) -> {
if (values == null) {
return;
}
if (values.size() == 1) {
target.put(String.valueOf(key), values.get(0));
return;
}
target.put(String.valueOf(key), new ArrayList<>(values));
});
return;
}
if (addition instanceof Map<?, ?> map) {
map.forEach((key, value) -> target.put(String.valueOf(key), value));
}
}
private WebClient.RequestHeadersSpec<?> buildRequest(WebClient client, HttpCallSpec callSpec, HttpRequestPayload requestPayload, Map<String, String> headerMap, boolean hasBody) {
URI uri = buildUri(callSpec, requestPayload, hasBody);
WebClient.RequestBodyUriSpec uriSpec = client.method(callSpec.method);
WebClient.RequestHeadersSpec<?> headersSpec = uriSpec.uri(uri)
.accept(MediaType.APPLICATION_JSON)
.headers(httpHeaders -> headerMap.forEach(httpHeaders::add));
if (hasBody) {
Object body = requestPayload.body() == null ? Collections.emptyMap() : requestPayload.body();
headersSpec = ((WebClient.RequestBodySpec) headersSpec)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(body));
}
return headersSpec;
}
private URI buildUri(HttpCallSpec callSpec, HttpRequestPayload requestPayload, boolean hasBody) {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(callSpec.url);
Map<String, Object> queryParams = new LinkedHashMap<>(requestPayload.queryParams());
if (!hasBody) {
mergeQueryParams(queryParams, requestPayload.body());
}
applyQueryParams(builder, queryParams);
return builder.build(true).toUri();
}
private Object extractCaseInsensitive(Map<?, ?> source, String... keys) {
if (source == null || source.isEmpty()) {
return null;
}
for (String key : keys) {
for (Map.Entry<?, ?> entry : source.entrySet()) {
if (key.equalsIgnoreCase(String.valueOf(entry.getKey()))) {
return entry.getValue();
}
}
}
return null;
}
private boolean containsKeyIgnoreCase(Map<?, ?> source, String... keys) {
if (source == null || source.isEmpty()) {
return false;
}
for (String key : keys) {
for (Object entryKey : source.keySet()) {
if (key.equalsIgnoreCase(String.valueOf(entryKey))) {
return true;
}
}
}
return false;
}
private record HttpRequestPayload(Object body, Map<String, Object> queryParams) {
private HttpRequestPayload {
Map<String, Object> safeQuery = queryParams == null
? Collections.emptyMap()
: Collections.unmodifiableMap(new LinkedHashMap<>(queryParams));
queryParams = safeQuery;
}
static HttpRequestPayload of(Object body, Map<String, Object> queryParams) {
return new HttpRequestPayload(body, queryParams);
}
Object snapshot(boolean includeBody) {
boolean hasQuery = queryParams != null && !queryParams.isEmpty();
boolean hasBody = includeBody && body != null;
if (hasQuery && hasBody) {
Map<String, Object> composite = new LinkedHashMap<>();
composite.put("query", new LinkedHashMap<>(queryParams));
composite.put("body", body);
return composite;
}
if (hasQuery) {
return new LinkedHashMap<>(queryParams);
}
if (hasBody) {
return body;
}
return includeBody ? body : null;
}
}
private boolean supportsRequestBody(HttpMethod method) {
if (method == null) {
return true;
}
return !(HttpMethod.GET.equals(method)
|| HttpMethod.DELETE.equals(method)
|| HttpMethod.HEAD.equals(method)
|| HttpMethod.OPTIONS.equals(method)
|| HttpMethod.TRACE.equals(method));
}
}

View File

@@ -0,0 +1,154 @@
package com.zt.plat.module.databus.framework.integration.gateway.step.impl;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition;
import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionExecutor;
import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionSpec;
import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionSpecParser;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiStepResult;
import com.zt.plat.module.databus.framework.integration.gateway.step.ApiStepHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.integration.core.GenericHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Map;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_RPC_ENDPOINT_INVALID;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_RPC_EXECUTION_FAILED;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_RPC_METHOD_NOT_FOUND;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_RPC_UNSUPPORTED_SIGNATURE;
/**
* Step handler performing intra-application RPC invocations via Spring beans.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RpcStepHandler implements ApiStepHandler {
private final ApplicationContext applicationContext;
private final ExpressionExecutor expressionExecutor;
@Override
public boolean supports(String stepType) {
return "RPC".equalsIgnoreCase(stepType);
}
@Override
public GenericHandler<ApiInvocationContext> build(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) {
BeanMethod beanMethod = parseEndpoint(stepDefinition.getStep().getTargetEndpoint());
ExpressionSpec requestSpec = ExpressionSpecParser.parse(stepDefinition.getStep().getRequestMappingExpr(), ExpressionTypeEnum.JSON);
ExpressionSpec responseSpec = ExpressionSpecParser.parse(stepDefinition.getStep().getResponseMappingExpr(), ExpressionTypeEnum.JSON);
return (context, headers) -> {
Instant start = Instant.now();
try {
Object arguments = requestSpec == null ? context.getRequestBody() : expressionExecutor.evaluate(requestSpec, context, context.getRequestBody(), headers);
Object result = invoke(beanMethod, arguments, context);
context.addStepResult(ApiStepResult.builder()
.stepId(stepDefinition.getStep().getId())
.stepType(stepDefinition.getStep().getType())
.request(arguments)
.response(result)
.success(true)
.elapsed(Duration.between(start, Instant.now()))
.build());
if (result instanceof Map<?, ?> map) {
map.forEach((key, value) -> context.getVariables().put(String.valueOf(key), value));
} else if (result != null) {
context.setResponseBody(result);
}
} catch (Exception ex) {
context.addStepResult(ApiStepResult.builder()
.stepId(stepDefinition.getStep().getId())
.stepType(stepDefinition.getStep().getType())
.success(false)
.errorMessage(ex.getMessage())
.elapsed(Duration.between(start, Instant.now()))
.build());
if (ex instanceof ServiceException serviceException) {
throw serviceException;
}
throw ServiceExceptionUtil.exception(API_STEP_RPC_EXECUTION_FAILED, ex.getMessage());
}
return context;
};
}
private Object invoke(BeanMethod beanMethod, Object argument, ApiInvocationContext context) throws Exception {
Object bean = applicationContext.getBean(beanMethod.beanName);
Method method = resolveMethod(bean, beanMethod.methodName, argument);
if (method == null) {
throw ServiceExceptionUtil.exception(API_STEP_RPC_METHOD_NOT_FOUND, beanMethod.beanName, beanMethod.methodName);
}
ReflectionUtils.makeAccessible(method);
if (method.getParameterCount() == 0) {
return method.invoke(bean);
}
if (method.getParameterCount() == 1) {
Class<?> parameterType = method.getParameterTypes()[0];
if (ApiInvocationContext.class.isAssignableFrom(parameterType)) {
return method.invoke(bean, context);
}
return method.invoke(bean, convertArgument(argument, parameterType));
}
if (method.getParameterCount() == 2) {
return method.invoke(bean, context, argument);
}
throw ServiceExceptionUtil.exception(API_STEP_RPC_UNSUPPORTED_SIGNATURE, beanMethod.methodName);
}
private Method resolveMethod(Object bean, String methodName, Object argument) {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(bean.getClass());
return Arrays.stream(methods)
.filter(method -> method.getName().equals(methodName))
.filter(method -> method.getParameterCount() <= 2)
.findFirst()
.orElse(null);
}
private Object convertArgument(Object argument, Class<?> targetType) {
if (argument == null) {
return null;
}
if (targetType.isAssignableFrom(argument.getClass())) {
return argument;
}
if (targetType.equals(String.class)) {
return String.valueOf(argument);
}
if (Number.class.isAssignableFrom(targetType) && argument instanceof Number number) {
if (targetType.equals(Integer.class)) {
return number.intValue();
}
if (targetType.equals(Long.class)) {
return number.longValue();
}
if (targetType.equals(Double.class)) {
return number.doubleValue();
}
}
return argument;
}
private BeanMethod parseEndpoint(String endpoint) {
if (!endpoint.contains("#")) {
throw ServiceExceptionUtil.exception(API_STEP_RPC_ENDPOINT_INVALID);
}
String[] parts = endpoint.split("#", 2);
return new BeanMethod(parts[0], parts[1]);
}
private record BeanMethod(String beanName, String methodName) {
}
}

View File

@@ -0,0 +1,74 @@
package com.zt.plat.module.databus.framework.integration.gateway.step.impl;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition;
import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionExecutor;
import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionSpec;
import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionSpecParser;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiStepResult;
import com.zt.plat.module.databus.framework.integration.gateway.step.ApiStepHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.integration.core.GenericHandler;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_SCRIPT_EXECUTION_FAILED;
/**
* Step handler executing JSON-based scripted expressions.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ScriptStepHandler implements ApiStepHandler {
private final ExpressionExecutor expressionExecutor;
@Override
public boolean supports(String stepType) {
return "SCRIPT".equalsIgnoreCase(stepType);
}
@Override
public GenericHandler<ApiInvocationContext> build(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) {
ExpressionSpec spec = ExpressionSpecParser.parse(stepDefinition.getStep().getTargetEndpoint(), ExpressionTypeEnum.JSON);
return (context, headers) -> {
Instant start = Instant.now();
try {
Object result = expressionExecutor.evaluate(spec, context, context.getRequestBody(), headers);
context.addStepResult(ApiStepResult.builder()
.stepId(stepDefinition.getStep().getId())
.stepType(stepDefinition.getStep().getType())
.success(true)
.response(result)
.elapsed(Duration.between(start, Instant.now()))
.build());
if (result instanceof java.util.Map<?, ?> map) {
map.forEach((key, value) -> context.getVariables().put(String.valueOf(key), value));
} else if (result != null) {
context.setResponseBody(result);
}
} catch (Exception ex) {
context.addStepResult(ApiStepResult.builder()
.stepId(stepDefinition.getStep().getId())
.stepType(stepDefinition.getStep().getType())
.success(false)
.errorMessage(ex.getMessage())
.elapsed(Duration.between(start, Instant.now()))
.build());
if (ex instanceof ServiceException serviceException) {
throw serviceException;
}
throw ServiceExceptionUtil.exception(API_STEP_SCRIPT_EXECUTION_FAILED, ex.getMessage());
}
return context;
};
}
}

View File

@@ -0,0 +1,57 @@
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.definition.ApiDefinitionPageReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSaveReqVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import java.util.List;
import java.util.Optional;
/**
* Service providing access to API definitions and their orchestration metadata.
*/
public interface ApiDefinitionService {
/**
* Load all active API definitions for bootstrap.
*/
List<ApiDefinitionAggregate> loadActiveDefinitions();
/**
* Lookup API definition by code and version.
*/
Optional<ApiDefinitionAggregate> findByCodeAndVersion(String apiCode, String version);
/**
* Refresh a specific definition by evicting cache and reloading from DB.
*/
Optional<ApiDefinitionAggregate> refresh(String apiCode, String version);
/**
* Lookup API definition aggregate by primary key.
*/
Optional<ApiDefinitionAggregate> findById(Long id);
/**
* Query API definitions with pagination.
*/
PageResult<ApiDefinitionDO> getPage(ApiDefinitionPageReqVO reqVO);
/**
* Create a new API definition with orchestration metadata.
*/
Long create(ApiDefinitionSaveReqVO reqVO);
/**
* Update an existing API definition with orchestration metadata.
*/
void update(ApiDefinitionSaveReqVO reqVO);
/**
* Delete API definition and related metadata.
*/
void delete(Long id);
}

View File

@@ -0,0 +1,46 @@
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.policy.ApiPolicyPageReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySaveReqVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO;
import java.util.List;
import java.util.Optional;
/**
* Authentication policy operations.
*/
public interface ApiPolicyAuthService {
/**
* Paginate policies.
*/
PageResult<ApiPolicyAuthDO> getPage(ApiPolicyPageReqVO reqVO);
/**
* Fetch all active policies for dropdowns.
*/
List<ApiPolicyAuthDO> getSimpleList();
/**
* Find policy detail.
*/
Optional<ApiPolicyAuthDO> get(Long id);
/**
* Create policy definition.
*/
Long create(ApiPolicySaveReqVO reqVO);
/**
* Update policy definition.
*/
void update(ApiPolicySaveReqVO reqVO);
/**
* Delete policy definition.
*/
void delete(Long id);
}

Some files were not shown because too many files have changed in this diff Show More