增加密码加密
This commit is contained in:
parent
f75a4b5f7e
commit
10e75b9769
989
backend/docs/frontend/部署流程图弹窗开发指南.md
Normal file
989
backend/docs/frontend/部署流程图弹窗开发指南.md
Normal file
@ -0,0 +1,989 @@
|
|||||||
|
# 部署流程图弹窗开发指南
|
||||||
|
|
||||||
|
## 1. 概述
|
||||||
|
|
||||||
|
本指南用于指导前端开发部署流程图可视化弹窗,展示部署工作流的执行状态、节点详情和统计信息。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. API 接口
|
||||||
|
|
||||||
|
### 2.1 接口信息
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
GET /api/v1/deploy-records/{deployRecordId}/flow-graph
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| deployRecordId | Long | 是 | 部署记录ID |
|
||||||
|
|
||||||
|
### 2.3 响应数据结构
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface DeployRecordFlowGraphDTO {
|
||||||
|
// ============ 部署记录基本信息 ============
|
||||||
|
deployRecordId: number; // 部署记录ID
|
||||||
|
businessKey: string; // 业务标识(UUID)
|
||||||
|
deployStatus: DeployStatus; // 部署状态
|
||||||
|
deployBy: string; // 部署人
|
||||||
|
deployRemark: string; // 部署备注
|
||||||
|
deployStartTime: string; // 部署开始时间(ISO格式)
|
||||||
|
deployEndTime: string; // 部署结束时间(ISO格式)
|
||||||
|
deployDuration: number; // 部署总时长(秒)
|
||||||
|
|
||||||
|
// ============ 业务上下文信息 ============
|
||||||
|
applicationName: string; // 应用名称
|
||||||
|
applicationCode: string; // 应用编码
|
||||||
|
environmentName: string; // 环境名称
|
||||||
|
teamName: string; // 团队名称
|
||||||
|
|
||||||
|
// ============ 工作流实例信息 ============
|
||||||
|
workflowInstanceId: number; // 工作流实例ID
|
||||||
|
processInstanceId: string; // 流程实例ID(Flowable)
|
||||||
|
workflowStatus: WorkflowStatus; // 工作流实例状态
|
||||||
|
workflowStartTime: string; // 工作流开始时间
|
||||||
|
workflowEndTime: string; // 工作流结束时间
|
||||||
|
workflowDuration: number; // 工作流总时长(秒)
|
||||||
|
|
||||||
|
// ============ 执行统计信息 ============
|
||||||
|
totalNodeCount: number; // 总节点数
|
||||||
|
executedNodeCount: number; // 已执行节点数
|
||||||
|
successNodeCount: number; // 成功节点数
|
||||||
|
failedNodeCount: number; // 失败节点数
|
||||||
|
runningNodeCount: number; // 运行中节点数
|
||||||
|
|
||||||
|
// ============ 流程图数据 ============
|
||||||
|
graph: WorkflowDefinitionGraph; // 流程图结构
|
||||||
|
nodeInstances: NodeInstanceDTO[]; // 节点执行状态列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// 节点实例DTO
|
||||||
|
interface NodeInstanceDTO {
|
||||||
|
id: number | null; // 节点实例ID(未执行节点为null)
|
||||||
|
nodeId: string; // 节点ID
|
||||||
|
nodeName: string; // 节点名称
|
||||||
|
nodeType: string; // 节点类型
|
||||||
|
status: NodeStatus; // 节点状态
|
||||||
|
startTime: string | null; // 开始时间
|
||||||
|
endTime: string | null; // 结束时间
|
||||||
|
duration: number | null; // 执行时长(秒)
|
||||||
|
errorMessage: string | null; // 错误信息(失败时)
|
||||||
|
processInstanceId: string; // 流程实例ID
|
||||||
|
createTime: string | null; // 创建时间
|
||||||
|
updateTime: string | null; // 更新时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// 流程图结构
|
||||||
|
interface WorkflowDefinitionGraph {
|
||||||
|
nodes: GraphNode[]; // 节点列表
|
||||||
|
edges: GraphEdge[]; // 边列表
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GraphNode {
|
||||||
|
id: string; // 节点ID
|
||||||
|
nodeCode: string; // 节点编码
|
||||||
|
nodeType: string; // 节点类型
|
||||||
|
nodeName: string; // 节点名称
|
||||||
|
position: { x: number; y: number }; // 节点位置
|
||||||
|
configs: Record<string, any>; // 节点配置
|
||||||
|
inputMapping: Record<string, any>; // 输入映射
|
||||||
|
outputs: OutputField[]; // 输出字段定义
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GraphEdge {
|
||||||
|
id: string; // 边ID
|
||||||
|
from: string; // 源节点ID
|
||||||
|
to: string; // 目标节点ID
|
||||||
|
name: string; // 边名称
|
||||||
|
config: {
|
||||||
|
type: string; // 边类型
|
||||||
|
condition: {
|
||||||
|
type: string; // 条件类型
|
||||||
|
priority: number; // 优先级
|
||||||
|
expression?: string; // 条件表达式
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 枚举类型
|
||||||
|
enum DeployStatus {
|
||||||
|
CREATED = 'CREATED', // 已创建
|
||||||
|
PENDING_APPROVAL = 'PENDING_APPROVAL', // 待审批
|
||||||
|
RUNNING = 'RUNNING', // 运行中
|
||||||
|
SUCCESS = 'SUCCESS', // 成功
|
||||||
|
FAILED = 'FAILED', // 失败
|
||||||
|
REJECTED = 'REJECTED', // 审批拒绝
|
||||||
|
CANCELLED = 'CANCELLED', // 已取消
|
||||||
|
TERMINATED = 'TERMINATED', // 已终止
|
||||||
|
PARTIAL_SUCCESS = 'PARTIAL_SUCCESS' // 部分成功
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NodeStatus {
|
||||||
|
NOT_STARTED = 'NOT_STARTED', // 未开始
|
||||||
|
RUNNING = 'RUNNING', // 运行中
|
||||||
|
SUSPENDED = 'SUSPENDED', // 已暂停
|
||||||
|
COMPLETED = 'COMPLETED', // 已完成
|
||||||
|
TERMINATED = 'TERMINATED', // 已终止
|
||||||
|
FAILED = 'FAILED' // 执行失败
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. UI 设计建议
|
||||||
|
|
||||||
|
### 3.1 整体布局
|
||||||
|
|
||||||
|
建议使用全屏或大尺寸弹窗(1200px+),分为三个主要区域:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 📋 头部信息区(部署基本信息 + 统计卡片) │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 🎨 流程图可视化区(Canvas绘制) │
|
||||||
|
│ - 节点按状态着色 │
|
||||||
|
│ - 支持缩放、拖拽 │
|
||||||
|
│ - 点击节点查看详情 │
|
||||||
|
│ │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 📊 底部详情区(选中节点的详细信息) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 头部信息区
|
||||||
|
|
||||||
|
显示关键信息和统计数据:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 第一行:基本信息
|
||||||
|
┌───────────────────────────────────────────────────────────┐
|
||||||
|
│ 🚀 部署流程详情 [关闭 ✕] │
|
||||||
|
├───────────────────────────────────────────────────────────┤
|
||||||
|
│ 应用:应用名称 (应用编码) | 环境:生产环境 | 团队:运维组 │
|
||||||
|
│ 部署人:张三 | 开始时间:2025-11-05 14:00:00 │
|
||||||
|
│ 状态:🟢 运行中 | 总时长:5分30秒 │
|
||||||
|
└───────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
// 第二行:统计卡片
|
||||||
|
┌──────────┬──────────┬──────────┬──────────┬──────────┐
|
||||||
|
│ 总节点 │ 已执行 │ 成功 │ 失败 │ 运行中 │
|
||||||
|
│ 8 │ 5 │ 4 │ 0 │ 1 │
|
||||||
|
│ 100% │ 62% │ 50% │ 0% │ 12% │
|
||||||
|
└──────────┴──────────┴──────────┴──────────┴──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 流程图可视化区
|
||||||
|
|
||||||
|
#### 节点状态着色方案
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const nodeColorMap = {
|
||||||
|
NOT_STARTED: '#d9d9d9', // 未开始:灰色
|
||||||
|
RUNNING: '#1890ff', // 运行中:蓝色(闪烁动画)
|
||||||
|
COMPLETED: '#52c41a', // 已完成:绿色
|
||||||
|
FAILED: '#ff4d4f', // 失败:红色
|
||||||
|
SUSPENDED: '#faad14', // 暂停:橙色
|
||||||
|
TERMINATED: '#8c8c8c' // 终止:深灰
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodeIconMap = {
|
||||||
|
START_EVENT: '▶️',
|
||||||
|
END_EVENT: '⏹️',
|
||||||
|
USER_TASK: '👤',
|
||||||
|
SERVICE_TASK: '⚙️',
|
||||||
|
APPROVAL: '✓',
|
||||||
|
SCRIPT_TASK: '📝',
|
||||||
|
JENKINS_BUILD: '🔨',
|
||||||
|
NOTIFICATION: '🔔'
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 节点显示内容
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ 🔨 Jenkins构建 │ ← 节点图标 + 名称
|
||||||
|
├─────────────────┤
|
||||||
|
│ ✓ 已完成 │ ← 状态(带图标)
|
||||||
|
│ ⏱ 2分30秒 │ ← 执行时长
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
失败节点特殊标记:
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ ⚙️ 部署服务 │
|
||||||
|
├─────────────────┤
|
||||||
|
│ ✗ 执行失败 │ ← 红色状态
|
||||||
|
│ ⚠️ 点击查看错误 │ ← 错误提示
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 底部详情区
|
||||||
|
|
||||||
|
选中节点后显示完整信息:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ 📌 节点详情 │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ 节点名称:Jenkins构建 │
|
||||||
|
│ 节点类型:SERVICE_TASK (服务任务) │
|
||||||
|
│ 执行状态:✓ 已完成 │
|
||||||
|
│ │
|
||||||
|
│ ⏱️ 时间信息 │
|
||||||
|
│ 开始时间:2025-11-05 14:02:30 │
|
||||||
|
│ 结束时间:2025-11-05 14:05:00 │
|
||||||
|
│ 执行时长:2分30秒 │
|
||||||
|
│ │
|
||||||
|
│ 📊 执行结果(如果有) │
|
||||||
|
│ 构建编号:#123 │
|
||||||
|
│ 构建状态:SUCCESS │
|
||||||
|
│ 构建产物:app-1.0.0.jar │
|
||||||
|
│ │
|
||||||
|
│ ⚠️ 错误信息(失败时显示) │
|
||||||
|
│ 连接超时:无法连接到Jenkins服务器 (timeout: 30s) │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 组件划分建议
|
||||||
|
|
||||||
|
### 4.1 组件层次结构
|
||||||
|
|
||||||
|
```
|
||||||
|
DeployFlowGraphModal (弹窗容器)
|
||||||
|
├── FlowGraphHeader (头部信息)
|
||||||
|
│ ├── DeployBasicInfo (基本信息)
|
||||||
|
│ └── ExecutionStatistics (统计卡片)
|
||||||
|
├── FlowGraphCanvas (流程图画布)
|
||||||
|
│ ├── GraphNode (节点组件)
|
||||||
|
│ └── GraphEdge (边组件)
|
||||||
|
└── NodeDetailPanel (节点详情面板)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 核心组件代码示例
|
||||||
|
|
||||||
|
#### 4.2.1 弹窗容器组件
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// DeployFlowGraphModal.tsx
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Modal, Spin, message } from 'antd';
|
||||||
|
import { getDeployFlowGraph } from '@/services/deploy';
|
||||||
|
import type { DeployRecordFlowGraphDTO, NodeInstanceDTO } from '@/types/deploy';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean;
|
||||||
|
deployRecordId: number;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeployFlowGraphModal: React.FC<Props> = ({
|
||||||
|
visible,
|
||||||
|
deployRecordId,
|
||||||
|
onClose
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [data, setData] = useState<DeployRecordFlowGraphDTO | null>(null);
|
||||||
|
const [selectedNode, setSelectedNode] = useState<NodeInstanceDTO | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible && deployRecordId) {
|
||||||
|
loadFlowGraph();
|
||||||
|
}
|
||||||
|
}, [visible, deployRecordId]);
|
||||||
|
|
||||||
|
const loadFlowGraph = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await getDeployFlowGraph(deployRecordId);
|
||||||
|
setData(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
message.error('加载流程图失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="部署流程详情"
|
||||||
|
open={visible}
|
||||||
|
onCancel={onClose}
|
||||||
|
width={1200}
|
||||||
|
footer={null}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
{data && (
|
||||||
|
<div className="deploy-flow-graph">
|
||||||
|
{/* 头部信息 */}
|
||||||
|
<FlowGraphHeader data={data} />
|
||||||
|
|
||||||
|
{/* 流程图画布 */}
|
||||||
|
<FlowGraphCanvas
|
||||||
|
graph={data.graph}
|
||||||
|
nodeInstances={data.nodeInstances}
|
||||||
|
selectedNode={selectedNode}
|
||||||
|
onNodeClick={setSelectedNode}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 节点详情面板 */}
|
||||||
|
{selectedNode && (
|
||||||
|
<NodeDetailPanel
|
||||||
|
node={selectedNode}
|
||||||
|
onClose={() => setSelectedNode(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Spin>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2.2 头部信息组件
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// FlowGraphHeader.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import { Tag, Statistic, Card, Row, Col, Descriptions } from 'antd';
|
||||||
|
import type { DeployRecordFlowGraphDTO } from '@/types/deploy';
|
||||||
|
import { formatDuration, getStatusTag } from '@/utils/deploy';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: DeployRecordFlowGraphDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FlowGraphHeader: React.FC<Props> = ({ data }) => {
|
||||||
|
return (
|
||||||
|
<div className="flow-graph-header">
|
||||||
|
{/* 基本信息 */}
|
||||||
|
<Descriptions column={3} size="small" bordered style={{ marginBottom: 16 }}>
|
||||||
|
<Descriptions.Item label="应用">
|
||||||
|
{data.applicationName} ({data.applicationCode})
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="环境">
|
||||||
|
{data.environmentName}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="团队">
|
||||||
|
{data.teamName}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="部署人">
|
||||||
|
{data.deployBy}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="开始时间">
|
||||||
|
{data.deployStartTime}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="状态">
|
||||||
|
{getStatusTag(data.deployStatus)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="总时长" span={3}>
|
||||||
|
{formatDuration(data.deployDuration)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
{data.deployRemark && (
|
||||||
|
<Descriptions.Item label="备注" span={3}>
|
||||||
|
{data.deployRemark}
|
||||||
|
</Descriptions.Item>
|
||||||
|
)}
|
||||||
|
</Descriptions>
|
||||||
|
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={4}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="总节点"
|
||||||
|
value={data.totalNodeCount}
|
||||||
|
suffix="个"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={5}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="已执行"
|
||||||
|
value={data.executedNodeCount}
|
||||||
|
suffix={`/ ${data.totalNodeCount}`}
|
||||||
|
valueStyle={{ color: '#1890ff' }}
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: 8, fontSize: 12, color: '#8c8c8c' }}>
|
||||||
|
{((data.executedNodeCount / data.totalNodeCount) * 100).toFixed(0)}%
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={5}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="成功"
|
||||||
|
value={data.successNodeCount}
|
||||||
|
valueStyle={{ color: '#52c41a' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={5}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="失败"
|
||||||
|
value={data.failedNodeCount}
|
||||||
|
valueStyle={{ color: data.failedNodeCount > 0 ? '#ff4d4f' : undefined }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={5}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="运行中"
|
||||||
|
value={data.runningNodeCount}
|
||||||
|
valueStyle={{ color: data.runningNodeCount > 0 ? '#1890ff' : undefined }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2.3 流程图画布组件(使用 AntV X6)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// FlowGraphCanvas.tsx
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { Graph } from '@antv/x6';
|
||||||
|
import type { WorkflowDefinitionGraph, NodeInstanceDTO } from '@/types/deploy';
|
||||||
|
import { getNodeColor, getNodeIcon } from '@/utils/workflow';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
graph: WorkflowDefinitionGraph;
|
||||||
|
nodeInstances: NodeInstanceDTO[];
|
||||||
|
selectedNode: NodeInstanceDTO | null;
|
||||||
|
onNodeClick: (node: NodeInstanceDTO) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FlowGraphCanvas: React.FC<Props> = ({
|
||||||
|
graph,
|
||||||
|
nodeInstances,
|
||||||
|
selectedNode,
|
||||||
|
onNodeClick
|
||||||
|
}) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const graphRef = useRef<Graph | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
// 创建画布
|
||||||
|
const graphInstance = new Graph({
|
||||||
|
container: containerRef.current,
|
||||||
|
width: 1100,
|
||||||
|
height: 500,
|
||||||
|
grid: true,
|
||||||
|
panning: true,
|
||||||
|
mousewheel: {
|
||||||
|
enabled: true,
|
||||||
|
modifiers: ['ctrl', 'meta'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
graphRef.current = graphInstance;
|
||||||
|
|
||||||
|
// 渲染节点
|
||||||
|
renderNodes(graphInstance);
|
||||||
|
|
||||||
|
// 渲染边
|
||||||
|
renderEdges(graphInstance);
|
||||||
|
|
||||||
|
// 监听节点点击
|
||||||
|
graphInstance.on('node:click', ({ node }) => {
|
||||||
|
const nodeId = node.id;
|
||||||
|
const nodeInstance = nodeInstances.find(n => n.nodeId === nodeId);
|
||||||
|
if (nodeInstance) {
|
||||||
|
onNodeClick(nodeInstance);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
graphInstance.dispose();
|
||||||
|
};
|
||||||
|
}, [graph, nodeInstances]);
|
||||||
|
|
||||||
|
const renderNodes = (graphInstance: Graph) => {
|
||||||
|
graph.nodes.forEach(node => {
|
||||||
|
const nodeInstance = nodeInstances.find(n => n.nodeId === node.id);
|
||||||
|
const status = nodeInstance?.status || 'NOT_STARTED';
|
||||||
|
const color = getNodeColor(status);
|
||||||
|
const icon = getNodeIcon(node.nodeType);
|
||||||
|
|
||||||
|
graphInstance.addNode({
|
||||||
|
id: node.id,
|
||||||
|
x: node.position.x,
|
||||||
|
y: node.position.y,
|
||||||
|
width: 120,
|
||||||
|
height: 80,
|
||||||
|
label: `${icon} ${node.nodeName}`,
|
||||||
|
attrs: {
|
||||||
|
body: {
|
||||||
|
fill: color,
|
||||||
|
stroke: status === selectedNode?.nodeId ? '#1890ff' : '#d9d9d9',
|
||||||
|
strokeWidth: status === selectedNode?.nodeId ? 3 : 1,
|
||||||
|
rx: 6,
|
||||||
|
ry: 6,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fill: '#ffffff',
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status,
|
||||||
|
duration: nodeInstance?.duration,
|
||||||
|
hasError: !!nodeInstance?.errorMessage,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加状态标签
|
||||||
|
if (nodeInstance) {
|
||||||
|
graphInstance.addNode({
|
||||||
|
id: `${node.id}-status`,
|
||||||
|
x: node.position.x + 5,
|
||||||
|
y: node.position.y + 60,
|
||||||
|
width: 110,
|
||||||
|
height: 18,
|
||||||
|
label: getStatusLabel(status, nodeInstance.duration),
|
||||||
|
attrs: {
|
||||||
|
body: {
|
||||||
|
fill: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
stroke: 'none',
|
||||||
|
rx: 3,
|
||||||
|
ry: 3,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fill: '#000000',
|
||||||
|
fontSize: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderEdges = (graphInstance: Graph) => {
|
||||||
|
graph.edges.forEach(edge => {
|
||||||
|
graphInstance.addEdge({
|
||||||
|
id: edge.id,
|
||||||
|
source: edge.from,
|
||||||
|
target: edge.to,
|
||||||
|
label: edge.name,
|
||||||
|
attrs: {
|
||||||
|
line: {
|
||||||
|
stroke: '#8c8c8c',
|
||||||
|
strokeWidth: 2,
|
||||||
|
targetMarker: {
|
||||||
|
name: 'classic',
|
||||||
|
size: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fill: '#595959',
|
||||||
|
fontSize: 11,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="flow-graph-canvas"
|
||||||
|
style={{ border: '1px solid #d9d9d9', marginTop: 16 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2.4 节点详情面板
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// NodeDetailPanel.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import { Card, Descriptions, Tag, Alert } from 'antd';
|
||||||
|
import type { NodeInstanceDTO } from '@/types/deploy';
|
||||||
|
import { formatDuration, getStatusTag } from '@/utils/deploy';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
node: NodeInstanceDTO;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NodeDetailPanel: React.FC<Props> = ({ node, onClose }) => {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title="📌 节点详情"
|
||||||
|
extra={<a onClick={onClose}>关闭</a>}
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
>
|
||||||
|
<Descriptions column={2} bordered size="small">
|
||||||
|
<Descriptions.Item label="节点名称" span={2}>
|
||||||
|
{node.nodeName}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="节点类型">
|
||||||
|
{node.nodeType}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="执行状态">
|
||||||
|
{getStatusTag(node.status)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
{node.startTime && (
|
||||||
|
<>
|
||||||
|
<Descriptions.Item label="开始时间">
|
||||||
|
{node.startTime}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="结束时间">
|
||||||
|
{node.endTime || '执行中...'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="执行时长" span={2}>
|
||||||
|
{node.duration ? formatDuration(node.duration) : '计算中...'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{node.status === 'NOT_STARTED' && (
|
||||||
|
<Descriptions.Item label="说明" span={2}>
|
||||||
|
<Tag color="default">该节点尚未执行</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
)}
|
||||||
|
</Descriptions>
|
||||||
|
|
||||||
|
{/* 错误信息 */}
|
||||||
|
{node.errorMessage && (
|
||||||
|
<Alert
|
||||||
|
message="执行错误"
|
||||||
|
description={node.errorMessage}
|
||||||
|
type="error"
|
||||||
|
showIcon
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 执行结果(outputs)*/}
|
||||||
|
{node.outputs && Object.keys(node.outputs).length > 0 && (
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<h4>📊 执行结果</h4>
|
||||||
|
<Descriptions column={1} bordered size="small">
|
||||||
|
{Object.entries(node.outputs).map(([key, value]) => (
|
||||||
|
<Descriptions.Item key={key} label={key}>
|
||||||
|
{typeof value === 'object' ? JSON.stringify(value) : String(value)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
))}
|
||||||
|
</Descriptions>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 工具函数
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// utils/deploy.ts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化时长
|
||||||
|
*/
|
||||||
|
export function formatDuration(seconds: number | null): string {
|
||||||
|
if (seconds === null || seconds === undefined) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}小时${minutes}分${secs}秒`;
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return `${minutes}分${secs}秒`;
|
||||||
|
} else {
|
||||||
|
return `${secs}秒`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态标签
|
||||||
|
*/
|
||||||
|
export function getStatusTag(status: string): React.ReactNode {
|
||||||
|
const statusConfig = {
|
||||||
|
NOT_STARTED: { color: 'default', text: '未开始' },
|
||||||
|
RUNNING: { color: 'processing', text: '运行中' },
|
||||||
|
COMPLETED: { color: 'success', text: '已完成' },
|
||||||
|
FAILED: { color: 'error', text: '执行失败' },
|
||||||
|
SUSPENDED: { color: 'warning', text: '已暂停' },
|
||||||
|
TERMINATED: { color: 'default', text: '已终止' },
|
||||||
|
|
||||||
|
CREATED: { color: 'default', text: '已创建' },
|
||||||
|
PENDING_APPROVAL: { color: 'warning', text: '待审批' },
|
||||||
|
SUCCESS: { color: 'success', text: '成功' },
|
||||||
|
REJECTED: { color: 'error', text: '审批拒绝' },
|
||||||
|
CANCELLED: { color: 'default', text: '已取消' },
|
||||||
|
PARTIAL_SUCCESS: { color: 'warning', text: '部分成功' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = statusConfig[status] || { color: 'default', text: status };
|
||||||
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取节点颜色
|
||||||
|
*/
|
||||||
|
export function getNodeColor(status: string): string {
|
||||||
|
const colorMap = {
|
||||||
|
NOT_STARTED: '#d9d9d9',
|
||||||
|
RUNNING: '#1890ff',
|
||||||
|
COMPLETED: '#52c41a',
|
||||||
|
FAILED: '#ff4d4f',
|
||||||
|
SUSPENDED: '#faad14',
|
||||||
|
TERMINATED: '#8c8c8c',
|
||||||
|
};
|
||||||
|
return colorMap[status] || '#d9d9d9';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取节点图标
|
||||||
|
*/
|
||||||
|
export function getNodeIcon(nodeType: string): string {
|
||||||
|
const iconMap = {
|
||||||
|
START_EVENT: '▶️',
|
||||||
|
END_EVENT: '⏹️',
|
||||||
|
USER_TASK: '👤',
|
||||||
|
SERVICE_TASK: '⚙️',
|
||||||
|
APPROVAL: '✓',
|
||||||
|
SCRIPT_TASK: '📝',
|
||||||
|
JENKINS_BUILD: '🔨',
|
||||||
|
NOTIFICATION: '🔔',
|
||||||
|
};
|
||||||
|
return iconMap[nodeType] || '⚙️';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态文本
|
||||||
|
*/
|
||||||
|
function getStatusLabel(status: string, duration: number | null): string {
|
||||||
|
const statusText = {
|
||||||
|
NOT_STARTED: '未开始',
|
||||||
|
RUNNING: '运行中',
|
||||||
|
COMPLETED: '已完成',
|
||||||
|
FAILED: '执行失败',
|
||||||
|
SUSPENDED: '已暂停',
|
||||||
|
TERMINATED: '已终止',
|
||||||
|
};
|
||||||
|
|
||||||
|
const text = statusText[status] || status;
|
||||||
|
const timeText = duration ? ` | ${formatDuration(duration)}` : '';
|
||||||
|
|
||||||
|
return `${text}${timeText}`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 高级功能建议
|
||||||
|
|
||||||
|
### 6.1 实时刷新
|
||||||
|
|
||||||
|
对于运行中的部署,建议实现定时刷新:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.runningNodeCount > 0) {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
loadFlowGraph(); // 每5秒刷新一次
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}
|
||||||
|
}, [data?.runningNodeCount]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 节点动画
|
||||||
|
|
||||||
|
运行中的节点添加闪烁动画:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0.6; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-running {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 进度条展示
|
||||||
|
|
||||||
|
在头部添加整体进度条:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Progress
|
||||||
|
percent={(data.executedNodeCount / data.totalNodeCount) * 100}
|
||||||
|
status={data.failedNodeCount > 0 ? 'exception' : 'active'}
|
||||||
|
format={(percent) => `${data.executedNodeCount} / ${data.totalNodeCount}`}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 节点搜索/过滤
|
||||||
|
|
||||||
|
添加节点名称搜索功能:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Input
|
||||||
|
placeholder="搜索节点"
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
onChange={(e) => handleSearchNode(e.target.value)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.5 导出功能
|
||||||
|
|
||||||
|
支持导出流程图为图片或PDF:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import html2canvas from 'html2canvas';
|
||||||
|
|
||||||
|
const exportFlowGraph = async () => {
|
||||||
|
const canvas = await html2canvas(containerRef.current);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = `deploy-flow-${deployRecordId}.png`;
|
||||||
|
link.href = canvas.toDataURL();
|
||||||
|
link.click();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 注意事项
|
||||||
|
|
||||||
|
### 7.1 性能优化
|
||||||
|
|
||||||
|
1. **虚拟滚动**:节点数量超过50个时使用虚拟滚动
|
||||||
|
2. **懒加载**:详情面板内容按需加载
|
||||||
|
3. **防抖节流**:缩放、拖拽等操作使用防抖
|
||||||
|
|
||||||
|
### 7.2 异常处理
|
||||||
|
|
||||||
|
1. **空数据处理**:显示"暂无流程图数据"提示
|
||||||
|
2. **加载失败**:提供重试按钮
|
||||||
|
3. **超时处理**:超过30秒显示超时提示
|
||||||
|
|
||||||
|
### 7.3 用户体验
|
||||||
|
|
||||||
|
1. **加载状态**:使用骨架屏或加载动画
|
||||||
|
2. **操作提示**:提供操作引导(缩放、拖拽说明)
|
||||||
|
3. **快捷键**:支持ESC关闭弹窗,Ctrl+滚轮缩放
|
||||||
|
|
||||||
|
### 7.4 移动端适配
|
||||||
|
|
||||||
|
如需移动端支持,建议:
|
||||||
|
- 使用响应式布局
|
||||||
|
- 触摸手势支持
|
||||||
|
- 简化节点显示信息
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 测试用例
|
||||||
|
|
||||||
|
### 8.1 基本功能测试
|
||||||
|
|
||||||
|
- [ ] 弹窗正常打开和关闭
|
||||||
|
- [ ] 数据正确加载和显示
|
||||||
|
- [ ] 节点正确渲染(位置、颜色、图标)
|
||||||
|
- [ ] 边正确连接
|
||||||
|
- [ ] 点击节点显示详情
|
||||||
|
|
||||||
|
### 8.2 状态测试
|
||||||
|
|
||||||
|
- [ ] 未开始节点显示为灰色
|
||||||
|
- [ ] 运行中节点显示蓝色并闪烁
|
||||||
|
- [ ] 成功节点显示绿色
|
||||||
|
- [ ] 失败节点显示红色并显示错误信息
|
||||||
|
|
||||||
|
### 8.3 交互测试
|
||||||
|
|
||||||
|
- [ ] 画布拖拽功能正常
|
||||||
|
- [ ] 画布缩放功能正常
|
||||||
|
- [ ] 节点选中高亮正常
|
||||||
|
- [ ] 统计数据准确
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 参考资源
|
||||||
|
|
||||||
|
### 9.1 推荐库
|
||||||
|
|
||||||
|
- **流程图渲染**:[AntV X6](https://x6.antv.vision/)
|
||||||
|
- **UI组件**:[Ant Design](https://ant.design/)
|
||||||
|
- **图表统计**:[AntV G2Plot](https://g2plot.antv.vision/)
|
||||||
|
|
||||||
|
### 9.2 设计参考
|
||||||
|
|
||||||
|
- Jenkins 构建详情页
|
||||||
|
- GitLab CI/CD Pipeline 可视化
|
||||||
|
- GitHub Actions 工作流可视化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 常见问题
|
||||||
|
|
||||||
|
**Q1: 节点太多时如何优化显示?**
|
||||||
|
A: 使用缩略图导航 + 局部放大;超过100个节点时考虑分页或折叠显示。
|
||||||
|
|
||||||
|
**Q2: 如何处理长时间运行的部署?**
|
||||||
|
A: 实现自动刷新(5-10秒间隔)+ WebSocket推送更新。
|
||||||
|
|
||||||
|
**Q3: 失败节点如何快速定位?**
|
||||||
|
A: 在头部添加"快速定位失败节点"按钮,点击自动滚动并高亮。
|
||||||
|
|
||||||
|
**Q4: 如何显示审批节点的审批人信息?**
|
||||||
|
A: 在节点 outputs 中包含 `approver`、`approvalTime` 等信息,详情面板中展示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 迭代计划
|
||||||
|
|
||||||
|
### V1.0(基础版)
|
||||||
|
- ✅ 流程图基本渲染
|
||||||
|
- ✅ 节点状态显示
|
||||||
|
- ✅ 统计信息展示
|
||||||
|
- ✅ 节点详情查看
|
||||||
|
|
||||||
|
### V2.0(增强版)
|
||||||
|
- 📋 实时刷新
|
||||||
|
- 📋 节点搜索/过滤
|
||||||
|
- 📋 导出功能
|
||||||
|
- 📋 操作历史记录
|
||||||
|
|
||||||
|
### V3.0(高级版)
|
||||||
|
- 📋 时间轴视图
|
||||||
|
- 📋 性能分析(节点耗时对比)
|
||||||
|
- 📋 历史部署对比
|
||||||
|
- 📋 智能建议(性能优化建议)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 联系方式
|
||||||
|
|
||||||
|
如有问题,请联系后端开发团队或查阅API文档。
|
||||||
|
|
||||||
@ -62,7 +62,20 @@ public class JenkinsServiceIntegrationImpl implements IJenkinsServiceIntegration
|
|||||||
public boolean testConnection(ExternalSystem system) {
|
public boolean testConnection(ExternalSystem system) {
|
||||||
try {
|
try {
|
||||||
String url = system.getUrl() + "/api/json";
|
String url = system.getUrl() + "/api/json";
|
||||||
|
|
||||||
|
// 详细日志:检查认证信息
|
||||||
|
log.info("===== Jenkins连接测试调试信息 =====");
|
||||||
|
log.info("URL: {}", url);
|
||||||
|
log.info("认证类型: {}", system.getAuthType());
|
||||||
|
log.info("用户名: {}", system.getUsername());
|
||||||
|
log.info("密码是否为空: {}", system.getPassword() == null || system.getPassword().isEmpty());
|
||||||
|
|
||||||
HttpHeaders headers = createHeaders(system);
|
HttpHeaders headers = createHeaders(system);
|
||||||
|
|
||||||
|
// 打印实际发送的请求头
|
||||||
|
log.info("Authorization头: {}", headers.getFirst("Authorization"));
|
||||||
|
log.info("===================================");
|
||||||
|
|
||||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||||
|
|
||||||
ResponseEntity<String> response = restTemplate.exchange(
|
ResponseEntity<String> response = restTemplate.exchange(
|
||||||
@ -106,8 +119,11 @@ public class JenkinsServiceIntegrationImpl implements IJenkinsServiceIntegration
|
|||||||
headers.set("Authorization", authHeader);
|
headers.set("Authorization", authHeader);
|
||||||
}
|
}
|
||||||
case TOKEN -> {
|
case TOKEN -> {
|
||||||
// Token认证
|
// Jenkins API Token认证也使用Basic Auth格式:username:apiToken
|
||||||
headers.set("Authorization", "Bearer " + jenkins.getToken());
|
String auth = jenkins.getUsername() + ":" + jenkins.getToken();
|
||||||
|
byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes());
|
||||||
|
String authHeader = "Basic " + new String(encodedAuth);
|
||||||
|
headers.set("Authorization", authHeader);
|
||||||
}
|
}
|
||||||
default -> throw new RuntimeException("Unsupported authentication type: " + jenkins.getAuthType());
|
default -> throw new RuntimeException("Unsupported authentication type: " + jenkins.getAuthType());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
|||||||
import com.qqchen.deploy.backend.framework.exception.BusinessException;
|
import com.qqchen.deploy.backend.framework.exception.BusinessException;
|
||||||
import com.qqchen.deploy.backend.framework.exception.UniqueConstraintException;
|
import com.qqchen.deploy.backend.framework.exception.UniqueConstraintException;
|
||||||
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
|
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
|
||||||
|
import com.qqchen.deploy.backend.framework.util.SensitiveDataEncryptor;
|
||||||
import com.qqchen.deploy.backend.deploy.integration.IExternalSystemIntegration;
|
import com.qqchen.deploy.backend.deploy.integration.IExternalSystemIntegration;
|
||||||
import com.qqchen.deploy.backend.system.model.ExternalSystemDTO;
|
import com.qqchen.deploy.backend.system.model.ExternalSystemDTO;
|
||||||
import com.qqchen.deploy.backend.deploy.repository.IExternalSystemRepository;
|
import com.qqchen.deploy.backend.deploy.repository.IExternalSystemRepository;
|
||||||
@ -88,6 +89,9 @@ public class ExternalSystemServiceImpl extends BaseServiceImpl<ExternalSystem, E
|
|||||||
@Resource
|
@Resource
|
||||||
private IRepositorySyncHistoryRepository repositorySyncHistoryRepository;
|
private IRepositorySyncHistoryRepository repositorySyncHistoryRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SensitiveDataEncryptor encryptor;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
integrationMap = systemIntegrations.stream()
|
integrationMap = systemIntegrations.stream()
|
||||||
@ -113,10 +117,51 @@ public class ExternalSystemServiceImpl extends BaseServiceImpl<ExternalSystem, E
|
|||||||
validateGitAuth(dto);
|
validateGitAuth(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExternalSystemDTO create(ExternalSystemDTO dto) {
|
||||||
|
// 加密密码和Token
|
||||||
|
encryptSensitiveData(dto);
|
||||||
|
return super.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ExternalSystemDTO update(Long id, ExternalSystemDTO dto) {
|
public ExternalSystemDTO update(Long id, ExternalSystemDTO dto) {
|
||||||
// 先验证Git认证
|
// 先验证Git认证
|
||||||
validateGitAuth(dto);
|
validateGitAuth(dto);
|
||||||
|
|
||||||
|
// 获取现有系统
|
||||||
|
ExternalSystem existingSystem = findEntityById(id);
|
||||||
|
|
||||||
|
// 密码处理
|
||||||
|
if (SensitiveDataEncryptor.isMasked(dto.getPassword())) {
|
||||||
|
// 如果是掩码,保留原密码
|
||||||
|
dto.setPassword(existingSystem.getPassword());
|
||||||
|
log.debug("保留原密码(掩码):系统ID={}", id);
|
||||||
|
} else if (StringUtils.isNotBlank(dto.getPassword())) {
|
||||||
|
// 如果是新密码,加密后保存
|
||||||
|
dto.setPassword(encryptor.encrypt(dto.getPassword()));
|
||||||
|
log.debug("更新并加密新密码:系统ID={}", id);
|
||||||
|
} else {
|
||||||
|
// 如果为空,保留原密码
|
||||||
|
dto.setPassword(existingSystem.getPassword());
|
||||||
|
log.debug("保留原密码(空值):系统ID={}", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token处理
|
||||||
|
if (SensitiveDataEncryptor.isMasked(dto.getToken())) {
|
||||||
|
// 如果是掩码,保留原Token
|
||||||
|
dto.setToken(existingSystem.getToken());
|
||||||
|
log.debug("保留原Token(掩码):系统ID={}", id);
|
||||||
|
} else if (StringUtils.isNotBlank(dto.getToken())) {
|
||||||
|
// 如果是新Token,加密后保存
|
||||||
|
dto.setToken(encryptor.encrypt(dto.getToken()));
|
||||||
|
log.debug("更新并加密新Token:系统ID={}", id);
|
||||||
|
} else {
|
||||||
|
// 如果为空,保留原Token
|
||||||
|
dto.setToken(existingSystem.getToken());
|
||||||
|
log.debug("保留原Token(空值):系统ID={}", id);
|
||||||
|
}
|
||||||
|
|
||||||
return super.update(id, dto);
|
return super.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,6 +181,22 @@ public class ExternalSystemServiceImpl extends BaseServiceImpl<ExternalSystem, E
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExternalSystemDTO findById(Long id) {
|
||||||
|
ExternalSystemDTO dto = super.findById(id);
|
||||||
|
// 查询后处理:用掩码替换敏感数据
|
||||||
|
maskSensitiveData(dto);
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ExternalSystemDTO> findAll() {
|
||||||
|
List<ExternalSystemDTO> dtos = super.findAll();
|
||||||
|
// 查询后处理:用掩码替换敏感数据
|
||||||
|
dtos.forEach(this::maskSensitiveData);
|
||||||
|
return dtos;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public boolean testConnection(Long id) {
|
public boolean testConnection(Long id) {
|
||||||
@ -144,12 +205,15 @@ public class ExternalSystemServiceImpl extends BaseServiceImpl<ExternalSystem, E
|
|||||||
throw new BusinessException(ResponseCode.EXTERNAL_SYSTEM_DISABLED);
|
throw new BusinessException(ResponseCode.EXTERNAL_SYSTEM_DISABLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解密密码和Token用于连接测试
|
||||||
|
ExternalSystem decryptedSystem = decryptSystemForUse(system);
|
||||||
|
|
||||||
IExternalSystemIntegration integration = integrationMap.get(system.getType());
|
IExternalSystemIntegration integration = integrationMap.get(system.getType());
|
||||||
if (integration == null) {
|
if (integration == null) {
|
||||||
throw new BusinessException(ResponseCode.EXTERNAL_SYSTEM_TYPE_NOT_SUPPORTED);
|
throw new BusinessException(ResponseCode.EXTERNAL_SYSTEM_TYPE_NOT_SUPPORTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean success = integration.testConnection(system);
|
boolean success = integration.testConnection(decryptedSystem);
|
||||||
if (success) {
|
if (success) {
|
||||||
system.setLastConnectTime(LocalDateTime.now());
|
system.setLastConnectTime(LocalDateTime.now());
|
||||||
externalSystemRepository.save(system);
|
externalSystemRepository.save(system);
|
||||||
@ -250,4 +314,60 @@ public class ExternalSystemServiceImpl extends BaseServiceImpl<ExternalSystem, E
|
|||||||
return instanceDTO;
|
return instanceDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密敏感数据(密码和Token)
|
||||||
|
* 在保存前调用
|
||||||
|
*/
|
||||||
|
private void encryptSensitiveData(ExternalSystemDTO dto) {
|
||||||
|
if (StringUtils.isNotBlank(dto.getPassword())) {
|
||||||
|
dto.setPassword(encryptor.encrypt(dto.getPassword()));
|
||||||
|
log.debug("密码已加密");
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(dto.getToken())) {
|
||||||
|
dto.setToken(encryptor.encrypt(dto.getToken()));
|
||||||
|
log.debug("Token已加密");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用掩码替换敏感数据
|
||||||
|
* 在查询后调用
|
||||||
|
*/
|
||||||
|
private void maskSensitiveData(ExternalSystemDTO dto) {
|
||||||
|
if (dto == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 如果有密码,用掩码替换
|
||||||
|
dto.setPassword(SensitiveDataEncryptor.maskIfPresent(dto.getPassword()));
|
||||||
|
// 如果有Token,用掩码替换
|
||||||
|
dto.setToken(SensitiveDataEncryptor.maskIfPresent(dto.getToken()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密系统信息用于实际调用外部系统
|
||||||
|
* 创建一个临时对象,不影响原对象
|
||||||
|
*/
|
||||||
|
private ExternalSystem decryptSystemForUse(ExternalSystem system) {
|
||||||
|
ExternalSystem decrypted = new ExternalSystem();
|
||||||
|
// 复制所有属性
|
||||||
|
decrypted.setId(system.getId());
|
||||||
|
decrypted.setName(system.getName());
|
||||||
|
decrypted.setType(system.getType());
|
||||||
|
decrypted.setUrl(system.getUrl());
|
||||||
|
decrypted.setAuthType(system.getAuthType());
|
||||||
|
decrypted.setUsername(system.getUsername());
|
||||||
|
decrypted.setEnabled(system.getEnabled());
|
||||||
|
decrypted.setConfig(system.getConfig());
|
||||||
|
|
||||||
|
// 解密敏感数据
|
||||||
|
if (StringUtils.isNotBlank(system.getPassword())) {
|
||||||
|
decrypted.setPassword(encryptor.decrypt(system.getPassword()));
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(system.getToken())) {
|
||||||
|
decrypted.setToken(encryptor.decrypt(system.getToken()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return decrypted;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -0,0 +1,221 @@
|
|||||||
|
package com.qqchen.deploy.backend.framework.util;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.encrypt.Encryptors;
|
||||||
|
import org.springframework.security.crypto.encrypt.TextEncryptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密解密工具类
|
||||||
|
* 可独立使用,无需启动Spring容器
|
||||||
|
*
|
||||||
|
* 使用场景:
|
||||||
|
* 1. 命令行工具:加密/解密敏感数据
|
||||||
|
* 2. 数据迁移:批量加密/解密数据库数据
|
||||||
|
* 3. 调试:验证加密结果
|
||||||
|
* 4. 紧急情况:在服务器上直接解密数据
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @since 2025-11-11
|
||||||
|
*/
|
||||||
|
public class EncryptionUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码掩码
|
||||||
|
*/
|
||||||
|
public static final String PASSWORD_MASK = "********";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认加密密钥(与配置文件一致)
|
||||||
|
*/
|
||||||
|
private static final String DEFAULT_SECRET_KEY = "deploy-ease-platform-secret-key-2025";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认加密盐值(与配置文件一致)
|
||||||
|
*/
|
||||||
|
private static final String DEFAULT_SALT = "a1b2c3d4e5f6a7b8";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用默认配置加密
|
||||||
|
*
|
||||||
|
* @param plainText 明文
|
||||||
|
* @return 密文
|
||||||
|
*/
|
||||||
|
public static String encrypt(String plainText) {
|
||||||
|
return encrypt(plainText, DEFAULT_SECRET_KEY, DEFAULT_SALT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用指定配置加密
|
||||||
|
*
|
||||||
|
* @param plainText 明文
|
||||||
|
* @param secretKey 密钥
|
||||||
|
* @param salt 盐值(16位十六进制字符串)
|
||||||
|
* @return 密文
|
||||||
|
*/
|
||||||
|
public static String encrypt(String plainText, String secretKey, String salt) {
|
||||||
|
if (plainText == null || plainText.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
TextEncryptor encryptor = Encryptors.text(secretKey, salt);
|
||||||
|
return encryptor.encrypt(plainText);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("加密失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用默认配置解密
|
||||||
|
*
|
||||||
|
* @param encryptedText 密文
|
||||||
|
* @return 明文
|
||||||
|
*/
|
||||||
|
public static String decrypt(String encryptedText) {
|
||||||
|
return decrypt(encryptedText, DEFAULT_SECRET_KEY, DEFAULT_SALT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用指定配置解密
|
||||||
|
*
|
||||||
|
* @param encryptedText 密文
|
||||||
|
* @param secretKey 密钥
|
||||||
|
* @param salt 盐值(16位十六进制字符串)
|
||||||
|
* @return 明文
|
||||||
|
*/
|
||||||
|
public static String decrypt(String encryptedText, String secretKey, String salt) {
|
||||||
|
if (encryptedText == null || encryptedText.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
TextEncryptor encryptor = Encryptors.text(secretKey, salt);
|
||||||
|
return encryptor.decrypt(encryptedText);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("解密失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为掩码
|
||||||
|
*
|
||||||
|
* @param value 要判断的值
|
||||||
|
* @return true表示是掩码
|
||||||
|
*/
|
||||||
|
public static boolean isMasked(String value) {
|
||||||
|
return PASSWORD_MASK.equals(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 命令行工具入口
|
||||||
|
* 用法:
|
||||||
|
* 1. 加密(使用默认配置):
|
||||||
|
* java EncryptionUtils encrypt "mypassword"
|
||||||
|
*
|
||||||
|
* 2. 解密(使用默认配置):
|
||||||
|
* java EncryptionUtils decrypt "encrypted_text_here"
|
||||||
|
*
|
||||||
|
* 3. 加密(指定配置):
|
||||||
|
* java EncryptionUtils encrypt "mypassword" "your-secret-key" "0123456789abcdef"
|
||||||
|
*
|
||||||
|
* 4. 解密(指定配置):
|
||||||
|
* java EncryptionUtils decrypt "encrypted_text" "your-secret-key" "0123456789abcdef"
|
||||||
|
*
|
||||||
|
* @param args 命令行参数
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) {
|
||||||
|
if (args.length < 2) {
|
||||||
|
printUsage();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String operation = args[0].toLowerCase();
|
||||||
|
String input = args[1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
String result;
|
||||||
|
if (args.length >= 4) {
|
||||||
|
// 使用指定的密钥和盐值
|
||||||
|
String secretKey = args[2];
|
||||||
|
String salt = args[3];
|
||||||
|
|
||||||
|
switch (operation) {
|
||||||
|
case "encrypt", "enc", "e" -> {
|
||||||
|
result = encrypt(input, secretKey, salt);
|
||||||
|
System.out.println("\n=== 加密成功 ===");
|
||||||
|
System.out.println("明文: " + input);
|
||||||
|
System.out.println("密文: " + result);
|
||||||
|
System.out.println("长度: " + result.length());
|
||||||
|
}
|
||||||
|
case "decrypt", "dec", "d" -> {
|
||||||
|
result = decrypt(input, secretKey, salt);
|
||||||
|
System.out.println("\n=== 解密成功 ===");
|
||||||
|
System.out.println("密文: " + input);
|
||||||
|
System.out.println("明文: " + result);
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
System.err.println("错误: 未知操作 '" + operation + "'");
|
||||||
|
printUsage();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 使用默认配置
|
||||||
|
switch (operation) {
|
||||||
|
case "encrypt", "enc", "e" -> {
|
||||||
|
result = encrypt(input);
|
||||||
|
System.out.println("\n=== 加密成功(使用默认配置) ===");
|
||||||
|
System.out.println("明文: " + input);
|
||||||
|
System.out.println("密文: " + result);
|
||||||
|
System.out.println("长度: " + result.length());
|
||||||
|
System.out.println("\n提示: 如需使用自定义配置,请提供密钥和盐值参数");
|
||||||
|
}
|
||||||
|
case "decrypt", "dec", "d" -> {
|
||||||
|
result = decrypt(input);
|
||||||
|
System.out.println("\n=== 解密成功(使用默认配置) ===");
|
||||||
|
System.out.println("密文: " + input);
|
||||||
|
System.out.println("明文: " + result);
|
||||||
|
System.out.println("\n提示: 如需使用自定义配置,请提供密钥和盐值参数");
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
System.err.println("错误: 未知操作 '" + operation + "'");
|
||||||
|
printUsage();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("\n操作失败: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印使用说明
|
||||||
|
*/
|
||||||
|
private static void printUsage() {
|
||||||
|
System.out.println("\n=== 加密解密工具 ===");
|
||||||
|
System.out.println("\n用法:");
|
||||||
|
System.out.println(" 使用默认配置:");
|
||||||
|
System.out.println(" java EncryptionUtils encrypt <明文>");
|
||||||
|
System.out.println(" java EncryptionUtils decrypt <密文>");
|
||||||
|
System.out.println();
|
||||||
|
System.out.println(" 使用自定义配置:");
|
||||||
|
System.out.println(" java EncryptionUtils encrypt <明文> <密钥> <盐值>");
|
||||||
|
System.out.println(" java EncryptionUtils decrypt <密文> <密钥> <盐值>");
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("示例:");
|
||||||
|
System.out.println(" # 加密密码(默认配置)");
|
||||||
|
System.out.println(" java EncryptionUtils encrypt \"mypassword123\"");
|
||||||
|
System.out.println();
|
||||||
|
System.out.println(" # 解密密码(默认配置)");
|
||||||
|
System.out.println(" java EncryptionUtils decrypt \"encrypted_text_here\"");
|
||||||
|
System.out.println();
|
||||||
|
System.out.println(" # 加密密码(自定义配置)");
|
||||||
|
System.out.println(" java EncryptionUtils encrypt \"mypassword\" \"my-secret-key-32-chars\" \"0123456789abcdef\"");
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("注意:");
|
||||||
|
System.out.println(" - 盐值必须是16位十六进制字符串(只能包含0-9和a-f)");
|
||||||
|
System.out.println(" - 密钥建议至少32位");
|
||||||
|
System.out.println(" - 使用的密钥和盐值必须与application.yml中的配置一致");
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
package com.qqchen.deploy.backend.framework.util;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 敏感数据加密服务类(Spring组件)
|
||||||
|
* 从配置文件读取密钥和盐值,委托给 EncryptionUtils 执行加密解密
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* - 本类是Spring组件,可自动注入,从配置文件读取密钥
|
||||||
|
* - 实际加密解密委托给 EncryptionUtils 静态工具类
|
||||||
|
* - EncryptionUtils 可独立使用,无需Spring容器
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @since 2025-11-11
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class SensitiveDataEncryptor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码掩码(用于前端显示)
|
||||||
|
*/
|
||||||
|
public static final String PASSWORD_MASK = EncryptionUtils.PASSWORD_MASK;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从配置文件读取加密密钥
|
||||||
|
*/
|
||||||
|
@Value("${deploy.encryption.secret-key}")
|
||||||
|
private String secretKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从配置文件读取加密盐值
|
||||||
|
*/
|
||||||
|
@Value("${deploy.encryption.salt}")
|
||||||
|
private String salt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密明文
|
||||||
|
*
|
||||||
|
* @param plainText 明文
|
||||||
|
* @return 加密后的文本,如果输入为空则返回null
|
||||||
|
*/
|
||||||
|
public String encrypt(String plainText) {
|
||||||
|
if (StringUtils.isBlank(plainText)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return EncryptionUtils.encrypt(plainText, secretKey, salt);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("加密失败", e);
|
||||||
|
throw new RuntimeException("数据加密失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密密文
|
||||||
|
*
|
||||||
|
* @param encryptedText 密文
|
||||||
|
* @return 解密后的明文,如果输入为空则返回null
|
||||||
|
*/
|
||||||
|
public String decrypt(String encryptedText) {
|
||||||
|
if (StringUtils.isBlank(encryptedText)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return EncryptionUtils.decrypt(encryptedText, secretKey, salt);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("解密失败,可能是明文或密钥错误", e);
|
||||||
|
// 如果解密失败,可能是旧数据(明文),直接返回
|
||||||
|
return encryptedText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断字符串是否为掩码
|
||||||
|
*
|
||||||
|
* @param value 要判断的字符串
|
||||||
|
* @return true表示是掩码
|
||||||
|
*/
|
||||||
|
public static boolean isMasked(String value) {
|
||||||
|
return EncryptionUtils.isMasked(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果有值则返回掩码,否则返回null
|
||||||
|
*
|
||||||
|
* @param value 原始值
|
||||||
|
* @return 掩码或null
|
||||||
|
*/
|
||||||
|
public static String maskIfPresent(String value) {
|
||||||
|
return StringUtils.isNotBlank(value) ? PASSWORD_MASK : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -100,3 +100,14 @@ jwt:
|
|||||||
|
|
||||||
jackson:
|
jackson:
|
||||||
time-zone: Asia/Shanghai
|
time-zone: Asia/Shanghai
|
||||||
|
|
||||||
|
# 部署平台配置
|
||||||
|
deploy:
|
||||||
|
# 敏感数据加密配置
|
||||||
|
encryption:
|
||||||
|
# 加密密钥(强烈建议使用环境变量 ENCRYPTION_SECRET_KEY)
|
||||||
|
# 生产环境务必修改为强随机字符串,密钥长度至少32位
|
||||||
|
secret-key: ${ENCRYPTION_SECRET_KEY:CHANGE-THIS-IN-PRODUCTION-ENV-32CHARS}
|
||||||
|
# 加密盐值(强烈建议使用环境变量 ENCRYPTION_SALT)
|
||||||
|
# 盐值必须是16位十六进制字符串(只能包含0-9和a-f)
|
||||||
|
salt: ${ENCRYPTION_SALT:0123456789abcdef}
|
||||||
|
|||||||
@ -100,3 +100,14 @@ jwt:
|
|||||||
|
|
||||||
jackson:
|
jackson:
|
||||||
time-zone: Asia/Shanghai
|
time-zone: Asia/Shanghai
|
||||||
|
|
||||||
|
# 部署平台配置
|
||||||
|
deploy:
|
||||||
|
# 敏感数据加密配置
|
||||||
|
encryption:
|
||||||
|
# 加密密钥(生产环境建议使用环境变量 ENCRYPTION_SECRET_KEY)
|
||||||
|
# 密钥长度至少32位,建议使用强随机字符串
|
||||||
|
secret-key: ${ENCRYPTION_SECRET_KEY:deploy-ease-platform-secret-key-2025}
|
||||||
|
# 加密盐值(生产环境建议使用环境变量 ENCRYPTION_SALT)
|
||||||
|
# 盐值必须是16位十六进制字符串(只能包含0-9和a-f)
|
||||||
|
salt: ${ENCRYPTION_SALT:a1b2c3d4e5f6a7b8}
|
||||||
|
|||||||
@ -542,8 +542,13 @@ VALUES (2, 304); -- 基础权限模板关联三方系统菜单
|
|||||||
-- --------------------------------------------------------------------------------------
|
-- --------------------------------------------------------------------------------------
|
||||||
-- 初始化外部系统数据
|
-- 初始化外部系统数据
|
||||||
-- --------------------------------------------------------------------------------------
|
-- --------------------------------------------------------------------------------------
|
||||||
|
-- 注意:password和token字段支持加密存储(AES-256)
|
||||||
|
-- 1. 初始化数据可以使用明文,首次编辑保存时会自动加密
|
||||||
|
-- 2. 表结构已扩展为VARCHAR(500)以容纳加密后的数据
|
||||||
|
-- 3. 生产环境建议通过管理界面添加外部系统,避免在SQL中暴露明文密码
|
||||||
|
-- --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
-- 初始化外部系统
|
-- 初始化外部系统(示例数据,生产环境请删除或修改)
|
||||||
INSERT INTO sys_external_system (
|
INSERT INTO sys_external_system (
|
||||||
id, create_by, create_time, deleted, update_by, update_time, version,
|
id, create_by, create_time, deleted, update_by, update_time, version,
|
||||||
name, type, url, remark, sort, enabled, auth_type, username, password, token,
|
name, type, url, remark, sort, enabled, auth_type, username, password, token,
|
||||||
@ -551,12 +556,12 @@ INSERT INTO sys_external_system (
|
|||||||
) VALUES (
|
) VALUES (
|
||||||
1, 'admin', '2023-12-01 00:00:00', 0, 'admin', '2023-12-01 00:00:00', 0,
|
1, 'admin', '2023-12-01 00:00:00', 0, 'admin', '2023-12-01 00:00:00', 0,
|
||||||
'链宇JENKINS', 'JENKINS', 'https://ly-jenkins.iscmtech.com', '链宇JENKINS', 1, 1,
|
'链宇JENKINS', 'JENKINS', 'https://ly-jenkins.iscmtech.com', '链宇JENKINS', 1, 1,
|
||||||
'BASIC', 'admin', 'Lianyu!@#~123456', NULL,
|
'BASIC', 'admin', 'Lianyu!@#~123456', NULL, -- 密码为明文,首次编辑会自动加密
|
||||||
'SUCCESS', '2023-12-01 00:00:00', '2023-12-01 00:00:00', '{}'
|
'SUCCESS', '2023-12-01 00:00:00', '2023-12-01 00:00:00', '{}'
|
||||||
), (
|
), (
|
||||||
2, 'admin', '2024-12-03 10:35:58.932966', 0, 'admin', '2024-12-03 10:35:58.932966', 0,
|
2, 'admin', '2024-12-03 10:35:58.932966', 0, 'admin', '2024-12-03 10:35:58.932966', 0,
|
||||||
'链宇GIT', 'GIT', 'https://ly-gitlab.iscmtech.com/', NULL, 1, 1,
|
'链宇GIT', 'GIT', 'https://ly-gitlab.iscmtech.com/', NULL, 1, 1,
|
||||||
'TOKEN', NULL, NULL, 'cNSud7D1GmYQKEMco7s5',
|
'TOKEN', NULL, NULL, 'cNSud7D1GmYQKEMco7s5', -- Token为明文,首次编辑会自动加密
|
||||||
NULL, NULL, NULL, '{}'
|
NULL, NULL, NULL, '{}'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -278,8 +278,8 @@ CREATE TABLE sys_external_system
|
|||||||
enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用(0:禁用,1:启用)',
|
enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用(0:禁用,1:启用)',
|
||||||
auth_type VARCHAR(50) NOT NULL COMMENT '认证方式(BASIC/TOKEN/OAUTH等)',
|
auth_type VARCHAR(50) NOT NULL COMMENT '认证方式(BASIC/TOKEN/OAUTH等)',
|
||||||
username VARCHAR(100) NULL COMMENT '用户名',
|
username VARCHAR(100) NULL COMMENT '用户名',
|
||||||
password VARCHAR(255) NULL COMMENT '密码',
|
password VARCHAR(500) NULL COMMENT '密码(加密存储,AES-256)',
|
||||||
token VARCHAR(255) NULL COMMENT '访问令牌',
|
token VARCHAR(500) NULL COMMENT '访问令牌(加密存储,AES-256)',
|
||||||
sync_status VARCHAR(50) NULL COMMENT '同步状态(SUCCESS/FAILED/RUNNING)',
|
sync_status VARCHAR(50) NULL COMMENT '同步状态(SUCCESS/FAILED/RUNNING)',
|
||||||
last_sync_time DATETIME(6) NULL COMMENT '最后同步时间',
|
last_sync_time DATETIME(6) NULL COMMENT '最后同步时间',
|
||||||
last_connect_time DATETIME(6) NULL COMMENT '最近连接成功时间',
|
last_connect_time DATETIME(6) NULL COMMENT '最近连接成功时间',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user