-
zhangwei
3 天以前 6002efe19de5fbf0ebf4f5192f3d9088f7588439
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
<template>
    <div class="sys-stress-test h100 overlay-none">
        <div>
            <NoticeBar text="接口压测会占用服务器大量的内存资源,请慎重操作!" style="margin: 4px" />
        </div>
        <splitpanes class="default-theme overlay-hidden">
            <pane size="20" class="vh100">
                <el-card class="vh80" shadow="hover" header="接口列表" v-loading="state.loading">
                    <el-row :gutter="35">
                        <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb10">
                            <el-select v-model="state.swaggerUrl" placeholder="接口分组" size="small">
                                <el-option :label="item.name" :value="item.url" v-for="(item, index) in state.groupList" :key="index" />
                            </el-select>
                        </el-col>
                        <el-col :xs="24" :sm="18" :md="18" :lg="18" :xl="18" class="mb10">
                            <el-input v-model="state.keywords" size="small" placeholder="关键字" clearable />
                        </el-col>
                        <el-col :xs="24" :sm="6" :md="6" :lg="6" :xl="6" class="mb10">
                            <el-button size="small" icon="ele-Search" v-reclick="1000" @click="queryTreeNode()" />
                        </el-col>
                    </el-row>
                    <el-tree
                        ref="treeRef"
                        class="filter-tree overlay-y vh68"
                        style="padding-bottom: 60px"
                        :data="state.data"
                        :props="{ children: 'children', label: 'summary' }"
                        :filter-node-method="filterNode"
                        node-key="id"
                        highlight-current
                        check-strictly>
                        <template #default="{ node }">
                            {{ node.label }}
                            <span class="node-button" v-if="!node.data.children">
                                <el-button size="small" icon="ele-DataLine" @click="treeNodeTest(node.data)" />
                            </span>
                        </template>
                    </el-tree>
                </el-card>
            </pane>
            <pane size="80" class="vh100">
                <el-card class="main-container vh80" shadow="hover" header="缓存数据" v-loading="state.loading" body-style="height:100vh; overflow:auto">
                    <template #header>
                        <el-button type="primary" @click="showDialog(undefined)">开始测试</el-button>
                    </template>
                    <el-descriptions title="压测参数" label-width="180px" :column="2" class="mb20" border>
                        <el-descriptions-item label="请求方式" label-align="left" align="left">
                            {{ state.ruleForm.requestMethod?.toUpperCase() }}
                        </el-descriptions-item>
                        <el-descriptions-item label="请求地址" label-align="left" align="left">
                            {{ state.ruleForm.requestUri }}
                        </el-descriptions-item>
                        <el-descriptions-item label="轮数" label-align="left" align="left">
                            {{ state.ruleForm.numberOfRounds ?? 0 }}
                        </el-descriptions-item>
                        <el-descriptions-item label="每轮请求数" label-align="left" align="left">
                            {{ state.ruleForm.numberOfRequests ?? 0 }}
                        </el-descriptions-item>
                        <el-descriptions-item label="最大并发量" label-align="left" align="left">
                            {{ state.ruleForm.maxDegreeOfParallelism ?? 0 }}
                        </el-descriptions-item>
                    </el-descriptions>
                    <el-descriptions title="压测结果" label-width="180px" :column="3" border>
                        <el-descriptions-item label="总用时(秒)" label-align="left" align="left">
                            {{ (state.result.totalTimeInSeconds ?? 0).toFixed(2) }}
                        </el-descriptions-item>
                        <el-descriptions-item label="成功请求次数" label-align="left" align="left">
                            {{ state.result.successfulRequests ?? 0 }}
                        </el-descriptions-item>
                        <el-descriptions-item label="失败请求次数" label-align="left" align="left">
                            {{ state.result.failedRequests ?? 0 }}
                        </el-descriptions-item>
                        <el-descriptions-item label="每秒查询率(QPS)" label-align="left" align="left">
                            {{ (state.result.queriesPerSecond ?? 0).toFixed(2) }}
                        </el-descriptions-item>
                        <el-descriptions-item label="最小响应时间(毫秒)" label-align="left" align="left">
                            {{ (state.result.minResponseTime ?? 0).toFixed(2) }}
                        </el-descriptions-item>
                        <el-descriptions-item label="最大响应时间(毫秒)" label-align="left" align="left">
                            {{ (state.result.maxResponseTime ?? 0).toFixed(2) }}
                        </el-descriptions-item>
                        <el-descriptions-item label="平均响应时间(毫秒)" span="3" label-align="left" align="left">
                            {{ (state.result.averageResponseTime ?? 0).toFixed(2) }}
                        </el-descriptions-item>
                        <el-descriptions-item label="P10 响应时间(毫秒)" label-align="left" align="left">
                            {{ (state.result.percentile10ResponseTime ?? 0).toFixed(2) }}
                        </el-descriptions-item>
                        <el-descriptions-item label="P25 响应时间(毫秒)" label-align="left" align="left">
                            {{ (state.result.percentile25ResponseTime ?? 0).toFixed(2) }}
                        </el-descriptions-item>
                        <el-descriptions-item label="P50 响应时间(毫秒)" label-align="left" align="left">
                            {{ (state.result.percentile50ResponseTime ?? 0).toFixed(2) }}
                        </el-descriptions-item>
                        <el-descriptions-item label="P75 响应时间(毫秒)" label-align="left" align="left">
                            {{ (state.result.percentile75ResponseTime ?? 0).toFixed(2) }}
                        </el-descriptions-item>
                        <el-descriptions-item label="P90 响应时间(毫秒)" label-align="left" align="left">
                            {{ (state.result.percentile90ResponseTime ?? 0).toFixed(2) }}
                        </el-descriptions-item>
                        <el-descriptions-item label="P99 响应时间(毫秒)" label-align="left" align="left">
                            {{ (state.result.percentile99ResponseTime ?? 0).toFixed(2) }}
                        </el-descriptions-item>
                        <el-descriptions-item label="P999 响应时间(毫秒)" label-align="left" align="left">
                            {{ (state.result.percentile999ResponseTime ?? 0).toFixed(2) }}
                        </el-descriptions-item>
                    </el-descriptions>
                </el-card>
            </pane>
        </splitpanes>
        <EditStressTest ref="editStressTestRef" @refreshData="refreshData" />
    </div>
</template>
 
<script lang="ts" setup name="sysStressTest">
import { onMounted, reactive, ref } from 'vue';
import EditStressTest from './component/editStressTest.vue';
import NoticeBar from '/@/components/noticeBar/index.vue';
import request, { getToken } from '/@/utils/request';
import { StressTestOutput } from "/@/api-services";
import { Splitpanes, Pane } from 'splitpanes';
import { ElTree } from 'element-plus';
import 'splitpanes/dist/splitpanes.css';
import 'vue-json-pretty/lib/styles.css';
 
const editStressTestRef = ref();
const treeRef = ref<InstanceType<typeof ElTree>>();
const state = reactive({
    loading: false,
    activeName: '',
    ruleForm: {
        requestUri: '',
        requestMethod: 'GET',
        numberOfRounds: 1,
        numberOfRequests: 100,
        maxDegreeOfParallelism: 200,
        requestParameters: [[]],
        queryParameters: [[]],
        pathParameters: [[]],
        headers: [[]],
    },
    keywords: undefined,
    swaggerUrl: '/swagger/Default/swagger.json',
    result: {} as StressTestOutput,
    data: [] as Array<any>,
    groupList: [],
});
 
onMounted(async () => {
    state.groupList = await getGroupList();
    state.data = await getApiList();
});
 
// 获取分组列表
const getGroupList = async () => {
    try {
        const html = await request(`/index.html`,{ method: 'get' }).then(({ data }) => data)
        const prefixText = 'var configObject = JSON.parse(\'';
        const jsonStr = html.substring(html.indexOf(prefixText) + prefixText.length, html.indexOf('var oauthConfigObject = JSON.parse('))?.trim().replace('\');', '');
        return JSON.parse(jsonStr).urls;
    } catch {
        return [];
    }
}
 
// 接口树节点按钮事件
const treeNodeTest = async (node: any) => {
    if (node.id == 0) return;
    state.ruleForm = {
        requestUri: location.origin + node.path,
        requestMethod: node.method,
        numberOfRounds: 1,
        numberOfRequests: 100,
        maxDegreeOfParallelism: 200,
        requestParameters: [],
        queryParameters: [],
        pathParameters: [],
        headers: [
            ['Authorization', 'Bearer ' + getToken()]
        ],
    };
    showDialog(state.ruleForm)
};
 
const showDialog = async (row: any) => {
    const newRow = row ?? { ...state.ruleForm };
    const convertToKeyValuePairs = (params) => {
        if (Array.isArray(params) && params.every(item => Array.isArray(item) && item.length === 2)) {
            return params
        } else if (typeof params === 'object' && params !== null) {
            return Object.entries(params)
        }
        return []
    }
 
    state.ruleForm = {
        ...newRow,
        requestParameters: convertToKeyValuePairs(newRow.requestParameters),
        queryParameters: convertToKeyValuePairs(newRow.queryParameters),
        pathParameters: convertToKeyValuePairs(newRow.pathParameters),
        headers: convertToKeyValuePairs(newRow.headers)
    }
    editStressTestRef.value.openDialog(state.ruleForm)
}
 
// 刷新数据
const refreshData = (data: StressTestOutput) => {
    state.result = data;
}
 
const getApiList = (keywords: string | undefined) => {
    const emojiPattern = /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu;
    return request(state.swaggerUrl,{ method: 'get' }).then(({ data }) => {
        const pathMap = data.paths;
        const result = data.tags.map((e: any) => ({ path: e.name, summary: e.description.replaceAll(emojiPattern, ''), children: [] }));
        Object.keys(pathMap).map(path => {
            const method = Object.keys(pathMap[path])[0];
            const apiInfo = pathMap[path][method];
            if (keywords && apiInfo.summary?.indexOf(keywords) === -1) return;
            result.find((u: any) => u.path === apiInfo.tags[0]).children.push({
                path: path,
                method: method,
                summary: apiInfo.summary?.replaceAll(emojiPattern, '') ?? path,
                parameters: apiInfo.parameters,
                requestBody: apiInfo.requestBody,
                data: apiInfo,
            });
        });
        return result.filter(u => u.children.length > 0);
    });
}
 
// 查询树节点
const queryTreeNode = async () => {
    state.data = await getApiList(state.keywords);
}
 
const filterNode = (value: string, data: any) => {
    if (!value) return true;
    return data.name.includes(value);
};
</script>
 
<style lang="scss" scoped>
.card-header {
    width: 100%;
    display: flex;
    justify-content: space-between;
    align-items: center;
}
:deep(.el-collapse-item) {
    .el-collapse-item__arrow {
        float: right;
    }
}
:deep(.main-container) {
    .el-card__header {
        padding: 8px;
    }
}
.node-button {
    position: absolute;
    scale: 0.7;
    right: 0;
}
</style>