refactor(系统设置): 配额管理

This commit is contained in:
shiziyuan9527 2022-03-29 11:40:28 +08:00 committed by shiziyuan9527
parent f1451d9a9a
commit bb2ece1d72
24 changed files with 846 additions and 50 deletions

View File

@ -1,6 +1,7 @@
package io.metersphere.base.domain;
import java.io.Serializable;
import java.math.BigDecimal;
import lombok.Data;
@Data
@ -23,5 +24,15 @@ public class Quota implements Serializable {
private Long updateTime;
private Integer member;
private Integer project;
private String projectId;
private BigDecimal vumTotal;
private BigDecimal vumUsed;
private static final long serialVersionUID = 1L;
}

View File

@ -1,5 +1,6 @@
package io.metersphere.base.domain;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@ -673,6 +674,316 @@ public class QuotaExample {
addCriterion("update_time not between", value1, value2, "updateTime");
return (Criteria) this;
}
public Criteria andMemberIsNull() {
addCriterion("`member` is null");
return (Criteria) this;
}
public Criteria andMemberIsNotNull() {
addCriterion("`member` is not null");
return (Criteria) this;
}
public Criteria andMemberEqualTo(Integer value) {
addCriterion("`member` =", value, "member");
return (Criteria) this;
}
public Criteria andMemberNotEqualTo(Integer value) {
addCriterion("`member` <>", value, "member");
return (Criteria) this;
}
public Criteria andMemberGreaterThan(Integer value) {
addCriterion("`member` >", value, "member");
return (Criteria) this;
}
public Criteria andMemberGreaterThanOrEqualTo(Integer value) {
addCriterion("`member` >=", value, "member");
return (Criteria) this;
}
public Criteria andMemberLessThan(Integer value) {
addCriterion("`member` <", value, "member");
return (Criteria) this;
}
public Criteria andMemberLessThanOrEqualTo(Integer value) {
addCriterion("`member` <=", value, "member");
return (Criteria) this;
}
public Criteria andMemberIn(List<Integer> values) {
addCriterion("`member` in", values, "member");
return (Criteria) this;
}
public Criteria andMemberNotIn(List<Integer> values) {
addCriterion("`member` not in", values, "member");
return (Criteria) this;
}
public Criteria andMemberBetween(Integer value1, Integer value2) {
addCriterion("`member` between", value1, value2, "member");
return (Criteria) this;
}
public Criteria andMemberNotBetween(Integer value1, Integer value2) {
addCriterion("`member` not between", value1, value2, "member");
return (Criteria) this;
}
public Criteria andProjectIsNull() {
addCriterion("project is null");
return (Criteria) this;
}
public Criteria andProjectIsNotNull() {
addCriterion("project is not null");
return (Criteria) this;
}
public Criteria andProjectEqualTo(Integer value) {
addCriterion("project =", value, "project");
return (Criteria) this;
}
public Criteria andProjectNotEqualTo(Integer value) {
addCriterion("project <>", value, "project");
return (Criteria) this;
}
public Criteria andProjectGreaterThan(Integer value) {
addCriterion("project >", value, "project");
return (Criteria) this;
}
public Criteria andProjectGreaterThanOrEqualTo(Integer value) {
addCriterion("project >=", value, "project");
return (Criteria) this;
}
public Criteria andProjectLessThan(Integer value) {
addCriterion("project <", value, "project");
return (Criteria) this;
}
public Criteria andProjectLessThanOrEqualTo(Integer value) {
addCriterion("project <=", value, "project");
return (Criteria) this;
}
public Criteria andProjectIn(List<Integer> values) {
addCriterion("project in", values, "project");
return (Criteria) this;
}
public Criteria andProjectNotIn(List<Integer> values) {
addCriterion("project not in", values, "project");
return (Criteria) this;
}
public Criteria andProjectBetween(Integer value1, Integer value2) {
addCriterion("project between", value1, value2, "project");
return (Criteria) this;
}
public Criteria andProjectNotBetween(Integer value1, Integer value2) {
addCriterion("project not between", value1, value2, "project");
return (Criteria) this;
}
public Criteria andProjectIdIsNull() {
addCriterion("project_id is null");
return (Criteria) this;
}
public Criteria andProjectIdIsNotNull() {
addCriterion("project_id is not null");
return (Criteria) this;
}
public Criteria andProjectIdEqualTo(String value) {
addCriterion("project_id =", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdNotEqualTo(String value) {
addCriterion("project_id <>", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdGreaterThan(String value) {
addCriterion("project_id >", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdGreaterThanOrEqualTo(String value) {
addCriterion("project_id >=", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdLessThan(String value) {
addCriterion("project_id <", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdLessThanOrEqualTo(String value) {
addCriterion("project_id <=", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdLike(String value) {
addCriterion("project_id like", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdNotLike(String value) {
addCriterion("project_id not like", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdIn(List<String> values) {
addCriterion("project_id in", values, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdNotIn(List<String> values) {
addCriterion("project_id not in", values, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdBetween(String value1, String value2) {
addCriterion("project_id between", value1, value2, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdNotBetween(String value1, String value2) {
addCriterion("project_id not between", value1, value2, "projectId");
return (Criteria) this;
}
public Criteria andVumTotalIsNull() {
addCriterion("vum_total is null");
return (Criteria) this;
}
public Criteria andVumTotalIsNotNull() {
addCriterion("vum_total is not null");
return (Criteria) this;
}
public Criteria andVumTotalEqualTo(BigDecimal value) {
addCriterion("vum_total =", value, "vumTotal");
return (Criteria) this;
}
public Criteria andVumTotalNotEqualTo(BigDecimal value) {
addCriterion("vum_total <>", value, "vumTotal");
return (Criteria) this;
}
public Criteria andVumTotalGreaterThan(BigDecimal value) {
addCriterion("vum_total >", value, "vumTotal");
return (Criteria) this;
}
public Criteria andVumTotalGreaterThanOrEqualTo(BigDecimal value) {
addCriterion("vum_total >=", value, "vumTotal");
return (Criteria) this;
}
public Criteria andVumTotalLessThan(BigDecimal value) {
addCriterion("vum_total <", value, "vumTotal");
return (Criteria) this;
}
public Criteria andVumTotalLessThanOrEqualTo(BigDecimal value) {
addCriterion("vum_total <=", value, "vumTotal");
return (Criteria) this;
}
public Criteria andVumTotalIn(List<BigDecimal> values) {
addCriterion("vum_total in", values, "vumTotal");
return (Criteria) this;
}
public Criteria andVumTotalNotIn(List<BigDecimal> values) {
addCriterion("vum_total not in", values, "vumTotal");
return (Criteria) this;
}
public Criteria andVumTotalBetween(BigDecimal value1, BigDecimal value2) {
addCriterion("vum_total between", value1, value2, "vumTotal");
return (Criteria) this;
}
public Criteria andVumTotalNotBetween(BigDecimal value1, BigDecimal value2) {
addCriterion("vum_total not between", value1, value2, "vumTotal");
return (Criteria) this;
}
public Criteria andVumUsedIsNull() {
addCriterion("vum_used is null");
return (Criteria) this;
}
public Criteria andVumUsedIsNotNull() {
addCriterion("vum_used is not null");
return (Criteria) this;
}
public Criteria andVumUsedEqualTo(BigDecimal value) {
addCriterion("vum_used =", value, "vumUsed");
return (Criteria) this;
}
public Criteria andVumUsedNotEqualTo(BigDecimal value) {
addCriterion("vum_used <>", value, "vumUsed");
return (Criteria) this;
}
public Criteria andVumUsedGreaterThan(BigDecimal value) {
addCriterion("vum_used >", value, "vumUsed");
return (Criteria) this;
}
public Criteria andVumUsedGreaterThanOrEqualTo(BigDecimal value) {
addCriterion("vum_used >=", value, "vumUsed");
return (Criteria) this;
}
public Criteria andVumUsedLessThan(BigDecimal value) {
addCriterion("vum_used <", value, "vumUsed");
return (Criteria) this;
}
public Criteria andVumUsedLessThanOrEqualTo(BigDecimal value) {
addCriterion("vum_used <=", value, "vumUsed");
return (Criteria) this;
}
public Criteria andVumUsedIn(List<BigDecimal> values) {
addCriterion("vum_used in", values, "vumUsed");
return (Criteria) this;
}
public Criteria andVumUsedNotIn(List<BigDecimal> values) {
addCriterion("vum_used not in", values, "vumUsed");
return (Criteria) this;
}
public Criteria andVumUsedBetween(BigDecimal value1, BigDecimal value2) {
addCriterion("vum_used between", value1, value2, "vumUsed");
return (Criteria) this;
}
public Criteria andVumUsedNotBetween(BigDecimal value1, BigDecimal value2) {
addCriterion("vum_used not between", value1, value2, "vumUsed");
return (Criteria) this;
}
}
public static class Criteria extends GeneratedCriteria {

View File

@ -11,6 +11,11 @@
<result column="workspace_id" jdbcType="VARCHAR" property="workspaceId" />
<result column="use_default" jdbcType="BIT" property="useDefault" />
<result column="update_time" jdbcType="BIGINT" property="updateTime" />
<result column="member" jdbcType="INTEGER" property="member" />
<result column="project" jdbcType="INTEGER" property="project" />
<result column="project_id" jdbcType="VARCHAR" property="projectId" />
<result column="vum_total" jdbcType="DECIMAL" property="vumTotal" />
<result column="vum_used" jdbcType="DECIMAL" property="vumUsed" />
</resultMap>
<sql id="Example_Where_Clause">
<where>
@ -72,7 +77,7 @@
</sql>
<sql id="Base_Column_List">
id, api, performance, max_threads, duration, resource_pool, workspace_id, use_default,
update_time
update_time, `member`, project, project_id, vum_total, vum_used
</sql>
<select id="selectByExample" parameterType="io.metersphere.base.domain.QuotaExample" resultMap="BaseResultMap">
select
@ -107,12 +112,14 @@
<insert id="insert" parameterType="io.metersphere.base.domain.Quota">
insert into quota (id, api, performance,
max_threads, duration, resource_pool,
workspace_id, use_default, update_time
)
workspace_id, use_default, update_time,
`member`, project, project_id,
vum_total, vum_used)
values (#{id,jdbcType=VARCHAR}, #{api,jdbcType=INTEGER}, #{performance,jdbcType=INTEGER},
#{maxThreads,jdbcType=INTEGER}, #{duration,jdbcType=INTEGER}, #{resourcePool,jdbcType=VARCHAR},
#{workspaceId,jdbcType=VARCHAR}, #{useDefault,jdbcType=BIT}, #{updateTime,jdbcType=BIGINT}
)
#{workspaceId,jdbcType=VARCHAR}, #{useDefault,jdbcType=BIT}, #{updateTime,jdbcType=BIGINT},
#{member,jdbcType=INTEGER}, #{project,jdbcType=INTEGER}, #{projectId,jdbcType=VARCHAR},
#{vumTotal,jdbcType=DECIMAL}, #{vumUsed,jdbcType=DECIMAL})
</insert>
<insert id="insertSelective" parameterType="io.metersphere.base.domain.Quota">
insert into quota
@ -144,6 +151,21 @@
<if test="updateTime != null">
update_time,
</if>
<if test="member != null">
`member`,
</if>
<if test="project != null">
project,
</if>
<if test="projectId != null">
project_id,
</if>
<if test="vumTotal != null">
vum_total,
</if>
<if test="vumUsed != null">
vum_used,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
@ -173,6 +195,21 @@
<if test="updateTime != null">
#{updateTime,jdbcType=BIGINT},
</if>
<if test="member != null">
#{member,jdbcType=INTEGER},
</if>
<if test="project != null">
#{project,jdbcType=INTEGER},
</if>
<if test="projectId != null">
#{projectId,jdbcType=VARCHAR},
</if>
<if test="vumTotal != null">
#{vumTotal,jdbcType=DECIMAL},
</if>
<if test="vumUsed != null">
#{vumUsed,jdbcType=DECIMAL},
</if>
</trim>
</insert>
<select id="countByExample" parameterType="io.metersphere.base.domain.QuotaExample" resultType="java.lang.Long">
@ -211,6 +248,21 @@
<if test="record.updateTime != null">
update_time = #{record.updateTime,jdbcType=BIGINT},
</if>
<if test="record.member != null">
`member` = #{record.member,jdbcType=INTEGER},
</if>
<if test="record.project != null">
project = #{record.project,jdbcType=INTEGER},
</if>
<if test="record.projectId != null">
project_id = #{record.projectId,jdbcType=VARCHAR},
</if>
<if test="record.vumTotal != null">
vum_total = #{record.vumTotal,jdbcType=DECIMAL},
</if>
<if test="record.vumUsed != null">
vum_used = #{record.vumUsed,jdbcType=DECIMAL},
</if>
</set>
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
@ -226,7 +278,12 @@
resource_pool = #{record.resourcePool,jdbcType=VARCHAR},
workspace_id = #{record.workspaceId,jdbcType=VARCHAR},
use_default = #{record.useDefault,jdbcType=BIT},
update_time = #{record.updateTime,jdbcType=BIGINT}
update_time = #{record.updateTime,jdbcType=BIGINT},
`member` = #{record.member,jdbcType=INTEGER},
project = #{record.project,jdbcType=INTEGER},
project_id = #{record.projectId,jdbcType=VARCHAR},
vum_total = #{record.vumTotal,jdbcType=DECIMAL},
vum_used = #{record.vumUsed,jdbcType=DECIMAL}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
@ -258,6 +315,21 @@
<if test="updateTime != null">
update_time = #{updateTime,jdbcType=BIGINT},
</if>
<if test="member != null">
`member` = #{member,jdbcType=INTEGER},
</if>
<if test="project != null">
project = #{project,jdbcType=INTEGER},
</if>
<if test="projectId != null">
project_id = #{projectId,jdbcType=VARCHAR},
</if>
<if test="vumTotal != null">
vum_total = #{vumTotal,jdbcType=DECIMAL},
</if>
<if test="vumUsed != null">
vum_used = #{vumUsed,jdbcType=DECIMAL},
</if>
</set>
where id = #{id,jdbcType=VARCHAR}
</update>
@ -270,7 +342,12 @@
resource_pool = #{resourcePool,jdbcType=VARCHAR},
workspace_id = #{workspaceId,jdbcType=VARCHAR},
use_default = #{useDefault,jdbcType=BIT},
update_time = #{updateTime,jdbcType=BIGINT}
update_time = #{updateTime,jdbcType=BIGINT},
`member` = #{member,jdbcType=INTEGER},
project = #{project,jdbcType=INTEGER},
project_id = #{projectId,jdbcType=VARCHAR},
vum_total = #{vumTotal,jdbcType=DECIMAL},
vum_used = #{vumUsed,jdbcType=DECIMAL}
where id = #{id,jdbcType=VARCHAR}
</update>
</mapper>

View File

@ -25,4 +25,6 @@ public interface ExtLoadTestReportMapper {
List<LoadTestReport> selectReportByProjectId(String projectId);
List<PlanReportCaseDTO> selectForPlanReport(@Param("ids") List<String> reportIds);
int updateReportVumStatus(String reportId,String reportKey ,String nextStatus, String preStatus);
}

View File

@ -186,4 +186,12 @@
and jmx_content is null
</update>
<update id="updateReportVumStatus">
update load_test_report_result
set report_value = #{nextStatus,jdbcType=VARCHAR}
WHERE report_id = #{reportId,jdbcType=VARCHAR}
and report_key = #{reportKey,jdbcType=VARCHAR}
and report_value = #{preStatus,jdbcType=VARCHAR}
</update>
</mapper>

View File

@ -22,4 +22,5 @@ public enum ReportKeys {
TimeInfo,
ResultStatus,
ReportCompleteCount,
VumProcessedStatus
}

View File

@ -0,0 +1,10 @@
package io.metersphere.performance.base;
/**
* @author lyh
*/
public class VumProcessedStatus {
public static final String PROCESSED = "Processed";
public static final String NOT_PROCESSED = "Not_Processed";
}

View File

@ -0,0 +1,77 @@
package io.metersphere.performance.service;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.LoadTestReportMapper;
import io.metersphere.base.mapper.ext.ExtLoadTestReportMapper;
import io.metersphere.commons.constants.PerformanceTestStatus;
import io.metersphere.commons.constants.ReportKeys;
import io.metersphere.commons.consumer.LoadTestFinishEvent;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.performance.base.VumProcessedStatus;
import io.metersphere.service.QuotaService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.math.BigDecimal;
/**
* @author lyh
*/
@Component
public class LoadTestVumEvent implements LoadTestFinishEvent {
@Resource
private LoadTestReportMapper loadTestReportMapper;
@Resource
private ExtLoadTestReportMapper extLoadTestReportMapper;
@Resource
private RedissonClient redissonClient;
private void handleVum(LoadTestReport report) {
if (report == null) {
return;
}
LoadTestReportWithBLOBs testReport = loadTestReportMapper.selectByPrimaryKey(report.getId());
if (testReport == null) {
return;
}
int bl = extLoadTestReportMapper.updateReportVumStatus(report.getId(),
ReportKeys.VumProcessedStatus.name(),
VumProcessedStatus.PROCESSED,
VumProcessedStatus.NOT_PROCESSED);
// 防止重复处理
if (bl == 0) {
return;
}
QuotaService quotaService = CommonBeanFactory.getBean(QuotaService.class);
String projectId = report.getProjectId();
RLock lock = redissonClient.getLock(projectId);
if (quotaService != null) {
try {
lock.lock();
BigDecimal toReduceVum = quotaService.getReduceVumUsed(testReport);
if (toReduceVum.compareTo(BigDecimal.ZERO) != 0) {
quotaService.updateVumUsed(projectId, toReduceVum.negate());
}
} finally {
lock.unlock();
}
} else {
LogUtil.error("handle vum event get quota service bean is null. load test report id: " + report.getId());
}
}
@Override
public void execute(LoadTestReport report) {
if (PerformanceTestStatus.Error.name().equals(report.getStatus())) {
// 失败后回退vum数量
this.handleVum(report);
}
}
}

View File

@ -28,12 +28,15 @@ import io.metersphere.performance.dto.LoadTestExportJmx;
import io.metersphere.performance.engine.Engine;
import io.metersphere.performance.engine.EngineFactory;
import io.metersphere.service.FileService;
import io.metersphere.service.QuotaService;
import io.metersphere.service.TestResourceService;
import io.metersphere.track.service.TestPlanLoadCaseService;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -42,6 +45,7 @@ import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
@ -74,6 +78,8 @@ public class PerformanceReportService {
private SqlSessionFactory sqlSessionFactory;
@Resource
private TestResourcePoolMapper testResourcePoolMapper;
@Resource
private RedissonClient redissonClient;
public List<ReportDTO> getRecentReportList(ReportRequest request) {
List<OrderRequest> orders = new ArrayList<>();
@ -111,9 +117,11 @@ public class PerformanceReportService {
boolean isRunning = StringUtils.equals(reportStatus, PerformanceTestStatus.Running.name());
boolean isStarting = StringUtils.equals(reportStatus, PerformanceTestStatus.Starting.name());
boolean isError = StringUtils.equals(reportStatus, PerformanceTestStatus.Error.name());
if (isRunning || isStarting || isError) {
if (isError) {
LogUtil.info("Start stop engine, report status: %s" + reportStatus);
stopEngine(loadTest, engine);
} else if (isRunning || isStarting) {
stopEngineHandleVum(loadTestReport, engine);
}
} catch (Exception e) {
LogUtil.error(e.getMessage(), e);
@ -157,6 +165,27 @@ public class PerformanceReportService {
loadTestMapper.updateByPrimaryKeySelective(loadTest);
}
public void stopEngineHandleVum(LoadTestReportWithBLOBs report, Engine engine) {
LoadTestWithBLOBs loadTest = loadTestMapper.selectByPrimaryKey(report.getTestId());
QuotaService quotaService = CommonBeanFactory.getBean(QuotaService.class);
String projectId = report.getProjectId();
RLock lock = redissonClient.getLock(projectId);
if (quotaService != null) {
try {
lock.lock();
BigDecimal toReduceVum = quotaService.getReduceVumUsed(report);
if (toReduceVum.compareTo(BigDecimal.ZERO) != 0) {
quotaService.updateVumUsed(projectId, toReduceVum.negate());
}
engine.stop();
loadTest.setStatus(PerformanceTestStatus.Saved.name());
loadTestMapper.updateByPrimaryKeySelective(loadTest);
} finally {
lock.unlock();
}
}
}
public ReportDTO getReportTestAndProInfo(String reportId) {
return extLoadTestReportMapper.getReportTestAndProInfo(reportId);
}

View File

@ -33,6 +33,7 @@ import io.metersphere.log.vo.DetailColumn;
import io.metersphere.log.vo.OperatingLogDetails;
import io.metersphere.log.vo.performance.PerformanceReference;
import io.metersphere.performance.base.GranularityData;
import io.metersphere.performance.base.VumProcessedStatus;
import io.metersphere.performance.dto.LoadModuleDTO;
import io.metersphere.performance.dto.LoadTestExportJmx;
import io.metersphere.performance.engine.Engine;
@ -53,6 +54,8 @@ import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.aspectj.util.FileUtil;
import org.mybatis.spring.SqlSessionUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -62,6 +65,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
@ -123,6 +127,8 @@ public class PerformanceTestService {
private ProjectMapper projectMapper;
@Resource
private ExtProjectVersionMapper extProjectVersionMapper;
@Resource
private RedissonClient redissonClient;
public List<LoadTestDTO> list(QueryTestPlanRequest request) {
request.setOrders(ServiceUtils.getDefaultSortOrder(request.getOrders()));
@ -291,7 +297,6 @@ public class PerformanceTestService {
}
public LoadTest edit(EditTestPlanRequest request, List<MultipartFile> files) {
checkQuota(request, false);
checkExist(request);
String testId = request.getId();
LoadTestWithBLOBs loadTest = loadTestMapper.selectByPrimaryKey(testId);
@ -472,6 +477,8 @@ public class PerformanceTestService {
testReport.setTestResourcePoolId(testPlanLoadCase.getTestResourcePoolId());
}
}
testReport.setStatus(PerformanceTestStatus.Starting.name());
testReport.setProjectId(loadTest.getProjectId());
testReport.setTestName(loadTest.getName());
@ -500,8 +507,15 @@ public class PerformanceTestService {
reportResult.setReportKey(ReportKeys.ResultStatus.name());
reportResult.setReportValue("Ready"); // 初始化一个 result_status, 这个值用在data-streaming中
loadTestReportResultMapper.insertSelective(reportResult);
// 启动测试
engine.start();
// 保存标记
LoadTestReportResult rr = new LoadTestReportResult();
rr.setId(UUID.randomUUID().toString());
rr.setReportId(testReport.getId());
rr.setReportKey(ReportKeys.VumProcessedStatus.name());
rr.setReportValue(VumProcessedStatus.NOT_PROCESSED); // 避免测试运行出错时对报告重复处理vum_used值
loadTestReportResultMapper.insertSelective(rr);
// 检查配额
this.checkLoadQuota(testReport, engine);
return testReport.getId();
} catch (MSException e) {
// 启动失败之后清理任务
@ -517,6 +531,29 @@ public class PerformanceTestService {
}
}
private void checkLoadQuota(LoadTestReportWithBLOBs testReport, Engine engine) {
RunTestPlanRequest checkRequest = new RunTestPlanRequest();
QuotaService quotaService = CommonBeanFactory.getBean(QuotaService.class);
checkRequest.setLoadConfiguration(testReport.getLoadConfiguration());
if (quotaService != null) {
quotaService.checkLoadTestQuota(checkRequest, false);
String projectId = testReport.getProjectId();
RLock lock = redissonClient.getLock(projectId);
try {
lock.lock();
BigDecimal toUsed = quotaService.checkVumUsed(checkRequest, projectId);
engine.start();
if (toUsed.compareTo(BigDecimal.ZERO) != 0) {
quotaService.updateVumUsed(projectId, toUsed);
}
} finally {
lock.unlock();
}
} else {
LogUtil.error("check load test quota fail, quotaService is null.");
}
}
public List<LoadTestDTO> recentTestPlans(QueryTestPlanRequest request) {
// 查询最近的测试计划
List<OrderRequest> orders = new ArrayList<>();
@ -641,7 +678,7 @@ public class PerformanceTestService {
if (forceStop) {
performanceReportService.deleteReport(reportId);
} else {
stopEngine(reportId);
stopEngineHandleVum(reportId);
// 停止测试之后设置报告的状态
performanceReportService.updateStatus(reportId, PerformanceTestStatus.Completed.name());
}
@ -663,6 +700,15 @@ public class PerformanceTestService {
performanceReportService.stopEngine(loadTest, engine);
}
private void stopEngineHandleVum(String reportId) {
LoadTestReportWithBLOBs loadTestReport = loadTestReportMapper.selectByPrimaryKey(reportId);
final Engine engine = EngineFactory.createEngine(loadTestReport);
if (engine == null) {
MSException.throwException(String.format("Stop report fail. create engine failreport ID%s", reportId));
}
performanceReportService.stopEngineHandleVum(loadTestReport, engine);
}
public List<ScheduleDao> listSchedule(QueryScheduleRequest request) {
request.setEnable(true);
List<ScheduleDao> schedules = scheduleService.list(request);

View File

@ -429,8 +429,10 @@ public class GroupService {
userGroup.setUpdateTime(System.currentTimeMillis());
userGroupMapper.insertSelective(userGroup);
} else {
QuotaService quotaService = CommonBeanFactory.getBean(QuotaService.class);
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
UserGroupMapper mapper = sqlSession.getMapper(UserGroupMapper.class);
checkQuota(quotaService, type, sourceIds, 1);
for (String sourceId : sourceIds) {
UserGroup userGroup = new UserGroup();
userGroup.setId(UUID.randomUUID().toString());
@ -448,6 +450,13 @@ public class GroupService {
}
}
private void checkQuota(QuotaService quotaService, String type, List<String> sourceIds, int size) {
if (quotaService != null) {
Map<String, Integer> addMemberMap = sourceIds.stream().collect(Collectors.toMap( id -> id, id -> size));
quotaService.checkMemberCount(addMemberMap, type);
}
}
public void editGroupUser(EditGroupUserRequest request) {
String groupId = request.getGroupId();
Group group = groupMapper.selectByPrimaryKey(groupId);

View File

@ -6,7 +6,6 @@ import io.metersphere.api.dto.QueryAPITestRequest;
import io.metersphere.api.service.APITestService;
import io.metersphere.api.service.ApiScenarioReportService;
import io.metersphere.api.service.ApiTestDelService;
import io.metersphere.api.service.ApiTestEnvironmentService;
import io.metersphere.api.tcp.TCPPool;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.*;
@ -127,6 +126,11 @@ public class ProjectService {
MSException.throwException(Translator.get("project_name_already_exists"));
}
QuotaService quotaService = CommonBeanFactory.getBean(QuotaService.class);
if (quotaService != null) {
quotaService.checkWorkspaceProject(project.getWorkspaceId());
}
if (project.getMockTcpPort() != null && project.getMockTcpPort().intValue() > 0) {
this.checkMockTcpPort(project.getMockTcpPort().intValue());
}

View File

@ -1,17 +1,76 @@
package io.metersphere.service;
import io.metersphere.base.domain.LoadTestReportWithBLOBs;
import io.metersphere.performance.request.TestPlanRequest;
import java.math.BigDecimal;
import java.util.Map;
import java.util.Set;
/**
* @author lyh
*/
public interface QuotaService {
/**
* api配额检查
*/
void checkAPIDefinitionQuota();
/**
* api配额检查
*/
void checkAPIAutomationQuota();
/**
* 性能测试配额检查
* @param request 压力配置
* @param checkPerformance 检查创建数量配额 / 检查并发数和时间
*/
void checkLoadTestQuota(TestPlanRequest request, boolean checkPerformance);
/**
* 检查资源池
* @return 资源池名称
*/
Set<String> getQuotaResourcePools();
/**
* 检查工作空间项目数量配额
* @param workspaceId 工作空间ID
*/
void checkWorkspaceProject(String workspaceId);
/**
* 检查vumUsed配额
* 未超过返回本次执行预计消耗的配额
* 超过抛出异常
* @param request 压力配置
* @param projectId 性能测试所属项目ID
* @return 本次执行预计消耗的配额
*/
BigDecimal checkVumUsed(TestPlanRequest request, String projectId);
/**
* 检查向某资源添加人员时是否超额
* @param map 资源ID:添加人数
* @param type 检查类型 PROJECT/WORKSPACE
*/
void checkMemberCount(Map<String, Integer> map, String type);
/**
* 更新VumUsed配额
* @param projectId 项目ID
* @param used 预计使用数量
*/
void updateVumUsed(String projectId, BigDecimal used);
/**
* 回退因主动停止或者测试启动后中途执行异常扣除的VumUsed配额
* @param report 性能测试报告
* @return 预计回退数量
*/
BigDecimal getReduceVumUsed(LoadTestReportWithBLOBs report);
}

View File

@ -126,6 +126,7 @@ public class UserService {
}
public void insertUserGroup(List<Map<String, Object>> groups, String userId) {
QuotaService quotaService = CommonBeanFactory.getBean(QuotaService.class);
for (Map<String, Object> map : groups) {
String idType = (String) map.get("type");
String[] arr = idType.split("\\+");
@ -142,6 +143,8 @@ public class UserService {
userGroupMapper.insertSelective(userGroup);
} else {
List<String> ids = (List<String>) map.get("ids");
Group group = groupMapper.selectByPrimaryKey(groupId);
checkQuota(quotaService, group.getType(), ids, 1);
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
UserGroupMapper mapper = sqlSession.getMapper(UserGroupMapper.class);
for (String id : ids) {
@ -163,6 +166,13 @@ public class UserService {
}
}
private void checkQuota(QuotaService quotaService, String type, List<String> sourceIds, int size) {
if (quotaService != null) {
Map<String, Integer> addMemberMap = sourceIds.stream().collect(Collectors.toMap( id -> id, id -> size));
quotaService.checkMemberCount(addMemberMap, type);
}
}
public User selectUser(String userId, String email) {
User user = userMapper.selectByPrimaryKey(userId);
if (user == null) {
@ -436,6 +446,10 @@ public class UserService {
public void addMember(AddMemberRequest request) {
if (!CollectionUtils.isEmpty(request.getUserIds())) {
QuotaService quotaService = CommonBeanFactory.getBean(QuotaService.class);
if (CollectionUtils.isNotEmpty(request.getUserIds())) {
checkQuota(quotaService, "WORKSPACE", Collections.singletonList(request.getWorkspaceId()), request.getUserIds().size());
}
for (String userId : request.getUserIds()) {
UserGroupExample userGroupExample = new UserGroupExample();
userGroupExample.createCriteria().andUserIdEqualTo(userId).andSourceIdEqualTo(request.getWorkspaceId());
@ -773,7 +787,11 @@ public class UserService {
}
}
QuotaService quotaService = CommonBeanFactory.getBean(QuotaService.class);
List<String> worksapceIds = request.getBatchProcessValue();
if (CollectionUtils.isNotEmpty(userIds)) {
checkQuota(quotaService, "WORKSPACE", worksapceIds, userIds.size());
}
for (String userId : userIds) {
UserGroupExample userGroupExample = new UserGroupExample();
userGroupExample
@ -816,7 +834,7 @@ public class UserService {
sourceMap.get(groupId).add(sourceId);
}
}
QuotaService quotaService = CommonBeanFactory.getBean(QuotaService.class);
for (String userId : userIds) {
Set<String> set = sourceMap.keySet();
for (String group : set) {
@ -844,6 +862,7 @@ public class UserService {
List<String> sourceIds = userGroups.stream().map(UserGroup::getSourceId).collect(Collectors.toList());
List<String> list = sourceMap.get(group);
list.removeAll(sourceIds);
checkQuota(quotaService, gp.getType(), list, 1);
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
UserGroupMapper mapper = sqlSession.getMapper(UserGroupMapper.class);
for (String sourceId : list) {
@ -884,7 +903,11 @@ public class UserService {
}
}
QuotaService quotaService = CommonBeanFactory.getBean(QuotaService.class);
List<String> projectIds = request.getBatchProcessValue();
if (CollectionUtils.isNotEmpty(userIds)) {
checkQuota(quotaService, "PROJECT", projectIds, userIds.size());
}
for (String userId : userIds) {
UserGroupExample userGroupExample = new UserGroupExample();
userGroupExample
@ -1088,7 +1111,10 @@ public class UserService {
LogUtil.info("add project member warning, request param user id list empty!");
return;
}
QuotaService quotaService = CommonBeanFactory.getBean(QuotaService.class);
if (CollectionUtils.isNotEmpty(request.getUserIds())) {
checkQuota(quotaService, "PROJECT", Collections.singletonList(request.getProjectId()), request.getUserIds().size());
}
for (String userId : request.getUserIds()) {
UserGroupExample userGroupExample = new UserGroupExample();
userGroupExample.createCriteria().andUserIdEqualTo(userId).andSourceIdEqualTo(request.getProjectId());
@ -1209,6 +1235,7 @@ public class UserService {
private void saveImportUserGroup(List<Map<String, Object>> groups, String userId) {
if (!groups.isEmpty()) {
QuotaService quotaService = CommonBeanFactory.getBean(QuotaService.class);
for (Map<String, Object> map : groups) {
String groupId = (String) map.get("id");
if (StringUtils.equals(groupId, UserGroupConstants.ADMIN)) {
@ -1222,6 +1249,8 @@ public class UserService {
userGroupMapper.insertSelective(userGroup);
} else {
List<String> ids = (List<String>) map.get("ids");
Group group = groupMapper.selectByPrimaryKey(groupId);
checkQuota(quotaService, group.getType(), ids, 1);
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
UserGroupMapper mapper = sqlSession.getMapper(UserGroupMapper.class);
for (String id : ids) {

View File

@ -180,3 +180,35 @@ CREATE TABLE IF NOT EXISTS `test_plan_execution_queue`
-- 场景步骤结果增加简要信息
ALTER TABLE api_scenario_report_result ADD `base_info` LONGTEXT NULL;
-- quota
-- 处理移除组织时遗留的脏数据
delete
from quota
where workspace_id is null
and id != 'workspace';
alter table quota
add member int(10) null comment '成员数量限制';
alter table quota
add project int(10) null comment '项目数量限制';
alter table quota
add project_id varchar(50) null comment '项目类型配额';
alter table quota
add vum_total decimal(10,2) null comment '总vum数';
alter table quota
add vum_used decimal(10,2) null comment '消耗的vum数';
INSERT INTO user_group_permission (id, group_id, permission_id, module_id)
VALUES (UUID(), 'ws_admin', 'WORKSPACE_QUOTA:READ', 'WORKSPACE_QUOTA') ;
INSERT INTO user_group_permission (id, group_id, permission_id, module_id)
VALUES (UUID(), 'ws_admin', 'WORKSPACE_QUOTA:READ+EDIT', 'WORKSPACE_QUOTA');

View File

@ -185,19 +185,25 @@ login_fail_filter_error=Login failed, please check the user filter
check_ldap_mapping=Check LDAP attribute mapping
ldap_mapping_value_null=LDAP user attribute mapping field is empty
#quota
quota_workspace_excess_org_api=The total number of interface tests in the workspace cannot exceed the organization's quota
quota_workspace_excess_org_performance=The total number of performance tests for the workspace cannot exceed the organization's quota
quota_workspace_excess_org_max_threads=The maximum concurrent number of workspaces cannot exceed the quota of the organization
quota_workspace_excess_org_max_duration=The stress test duration of the workspace cannot exceed the organization's quota
quota_workspace_excess_org_resource_pool=The resource pool of the workspace cannot exceed the resource pool of the organization
quota_project_excess_ws_api=The total number of interface tests for a project cannot exceed the workspace quota
quota_project_excess_ws_performance=The total number of performance tests for a project cannot exceed the workspace quota
quota_project_excess_ws_max_threads=The maximum concurrent number of projects cannot exceed the quota of the workspace
quota_project_excess_ws_max_duration=The stress test duration of the project cannot exceed the workspace quota
quota_project_excess_ws_resource_pool=The resource pool of the project cannot exceed the scope of the resource pool of the workspace
quota_project_excess_ws_vum_total=The sum of the total number of vums of the project cannot exceed the workspace quota
quota_api_excess_workspace=The number of interface tests exceeds the workspace quota
quota_api_excess_organization=The number of interface tests exceeds the organization quota
quota_api_excess_project=The number of interface tests exceeds the project limit
quota_performance_excess_workspace=The number of performance tests exceeds the workspace quota
quota_performance_excess_organization=The number of performance tests exceeds the organization quota
quota_performance_excess_project=The number of performance tests exceeds the project limit
quota_max_threads_excess_workspace=The maximum number of concurrent threads exceeds the workspace quota
quota_max_threads_excess_organization=The maximum number of concurrent threads exceeds the organization quota
quota_max_threads_excess_project=The maximum concurrent number exceeds the project limit
quota_duration_excess_workspace=The stress test duration exceeds the work space quota
quota_duration_excess_organization=The stress test duration exceeds the organization quota
quota_duration_excess_project=The stress test time exceeds the project limit
quota_member_excess_workspace=The number of members exceeds the workspace quota
quota_member_excess_project=The number of members exceeds the project quota
quota_project_excess_project=Number of projects exceeds workspace quota
quota_vum_used_excess_workspace=The amount of vum consumed exceeds the workspace quota
quota_vum_used_excess_project=The amount of vum consumed exceeds the project quota
import_xmind_count_error=The number of use cases imported into the mind map cannot exceed 800
import_xmind_not_found=Test case not found
license_valid_license_error=Authorization authentication failed

View File

@ -185,19 +185,25 @@ login_fail_filter_error=登录失败,请检查用户过滤器
check_ldap_mapping=检查LDAP属性映射
ldap_mapping_value_null=LDAP用户属性映射字段为空值
#quota
quota_workspace_excess_org_api=工作空间的接口测试数量总和不能超过组织的配额
quota_workspace_excess_org_performance=工作空间的性能测试数量总和不能超过组织的配额
quota_workspace_excess_org_max_threads=工作空间的最大并发数不能超过组织的配额
quota_workspace_excess_org_max_duration=工作空间的压测时长不能超过组织的配额
quota_workspace_excess_org_resource_pool=工作空间的资源池不能超过组织的资源池范围
quota_project_excess_ws_api=项目的接口测试数量总和不能超过工作空间的配额
quota_project_excess_ws_performance=项目的性能测试数量总和不能超过工作空间的配额
quota_project_excess_ws_max_threads=项目的最大并发数不能超过工作空间的配额
quota_project_excess_ws_max_duration=项目的压测时长不能超过工作空间的配额
quota_project_excess_ws_resource_pool=项目的资源池不能超过工作空间的资源池范围
quota_project_excess_ws_vum_total=项目的总vum数量总和不能超过工作空间配额
quota_api_excess_workspace=接口测试数量超过工作空间限额
quota_api_excess_organization=接口测试数量超过组织限额
quota_api_excess_project=接口测试数量超过项目限额
quota_performance_excess_workspace=性能测试数量超过工作空间限额
quota_performance_excess_organization=性能测试数量超过组织限额
quota_performance_excess_project=性能测试数量超过项目限额
quota_max_threads_excess_workspace=最大并发数超过工作空间限额
quota_max_threads_excess_organization=最大并发数超过组织限额
quota_max_threads_excess_project=最大并发数超过项目限额
quota_duration_excess_workspace=压测时长超过工作空间限额
quota_duration_excess_organization=压测时长超过组织限额
quota_duration_excess_project=压测时长超过项目限额
quota_member_excess_workspace=成员数超过工作空间配额
quota_member_excess_project=成员数超过项目配额
quota_project_excess_project=项目数超过工作空间配额
quota_vum_used_excess_workspace=消耗的vum数量超过工作空间配额
quota_vum_used_excess_project=消耗的vum数量超过项目配额
import_xmind_count_error=思维导图导入用例数量不能超过 800 条
license_valid_license_error=授权认证失败
import_xmind_not_found=未找到测试用例

View File

@ -185,19 +185,24 @@ login_fail_filter_error=登錄失敗,請檢查用戶過濾器
check_ldap_mapping=檢查LDAP屬性映射
ldap_mapping_value_null=LDAP用戶屬性映射字段為空值
#quota
quota_workspace_excess_org_api=工作空間的接口測試數量總和不能超過組織的配額
quota_workspace_excess_org_performance=工作空間的性能測試數量總和不能超過組織的配額
quota_workspace_excess_org_max_threads=工作空間的最大並發數不能超過組織的配額
quota_workspace_excess_org_max_duration=工作空間的壓測時長不能超過組織的配額
quota_workspace_excess_org_resource_pool=工作空間的資源池不能超過組織的資源池範圍
quota_project_excess_ws_api=項目的接口測試數量總和不能超過工作空間的配額
quota_project_excess_ws_performance=項目的性能測試數量總和不能超過工作空間的配額
quota_project_excess_ws_max_threads=項目的最大並發數不能超過工作空間的配額
quota_project_excess_ws_max_duration=項目的壓測時長不能超過工作空間的配額
quota_project_excess_ws_resource_pool=項目的資源池不能超過工作空間的資源池範圍
quota_project_excess_ws_vum_total=項目的總vum數量總和不能超過工作空間配額
quota_api_excess_workspace=接口測試數量超過工作空間限額
quota_api_excess_organization=接口測試數量超過組織限額
quota_api_excess_project=接口測試數量超過項目限額
quota_performance_excess_workspace=性能測試數量超過工作空間限額
quota_performance_excess_organization=性能測試數量超過組織限額
quota_performance_excess_project=性能測試數量超過項目限額
quota_max_threads_excess_workspace=最大並發數超過工作空間限額
quota_max_threads_excess_organization=最大並發數超過組織限額
quota_duration_excess_workspace=壓測時長超過工作空間限額
quota_duration_excess_organization=壓測時長超過組織限額
quota_max_threads_excess_project=最大並發數超過項目限額
quota_duration_excess_project=壓測時長超過項目限額
quota_member_excess_workspace=成員數超過工作空間配額
quota_member_excess_project=成員數超過項目配額
quota_project_excess_project=項目數超過工作空間配額
quota_vum_used_excess_workspace=消耗的vum數量超過工作空間配額
quota_vum_used_excess_project=消耗的vum數量超過項目配額
import_xmind_count_error=思維導圖導入用例數量不能超過 800 條
license_valid_license_error=授權認證失敗
import_xmind_not_found=未找到測試用例

View File

@ -240,6 +240,18 @@
"name": "permission.workspace_project_environment.delete_group",
"resourceId": "WORKSPACE_PROJECT_ENVIRONMENT"
},
{
"id": "WORKSPACE_QUOTA:READ",
"name": "permission.workspace_quota.read",
"resourceId": "WORKSPACE_QUOTA",
"license": true
},
{
"id": "WORKSPACE_QUOTA:READ+EDIT",
"name": "permission.workspace_quota.edit",
"resourceId": "WORKSPACE_QUOTA",
"license": true
},
{
"id": "WORKSPACE_OPERATING_LOG:READ",
"name": "permission.workspace_operation_log.read",
@ -1116,6 +1128,11 @@
"id": "WORKSPACE_PROJECT_ENVIRONMENT",
"name": "permission.workspace_project_environment.name"
},
{
"id": "WORKSPACE_QUOTA",
"name": "permission.workspace_quota.name",
"license": true
},
{
"id": "WORKSPACE_OPERATING_LOG",
"name": "permission.workspace_operation_log.name"

View File

@ -70,6 +70,7 @@ export default {
component: () => import('@/business/components/settings/workspace/MsProject'),
meta: {workspace: true, title: 'project.manager', permissions: ['WORKSPACE_PROJECT_MANAGER:READ']}
},
...requireContext.keys().map(key => requireContext(key).quota),
{
path: 'wsenvlist',
component: () => import('@/business/components/settings/workspace/environment/EnvironmentManage'),

View File

@ -263,6 +263,35 @@ export let CUSTOM_TABLE_HEADER = {
{id: 'updateTime', key: '8', label: 'commons.update_time'},
],
//空间配额
QUOTA_WS_LIST: [
{id: 'workspaceName', key: 'a', label: 'commons.workspace'},
{id: 'api', key: 'b', label: 'quota.api'},
{id: 'performance', key: 'c', label: 'quota.performance'},
{id: 'maxThreads', key: 'd', label: 'quota.max_threads'},
{id: 'duration', key: 'e', label: 'quota.duration'},
{id: 'resourcePool', key: 'f', label: 'quota.resource_pool'},
{id: 'useDefault', key: 'j', label: 'quota.use_default'},
{id: 'vumTotal', key: 'h', label: 'quota.vum_total'},
{id: 'vumUsed', key: 'i', label: 'quota.vum_used'},
{id: 'member', key: 'g', label: 'quota.member'},
{id: 'project', key: 'k', label: 'quota.project'},
],
//项目配额
QUOTA_PJ_LIST: [
{id: 'projectName', key: 'a', label: 'commons.project'},
{id: 'api', key: 'b', label: 'quota.api'},
{id: 'performance', key: 'c', label: 'quota.performance'},
{id: 'maxThreads', key: 'd', label: 'quota.max_threads'},
{id: 'duration', key: 'e', label: 'quota.duration'},
{id: 'resourcePool', key: 'f', label: 'quota.resource_pool'},
{id: 'useDefault', key: 'j', label: 'quota.use_default'},
{id: 'vumTotal', key: 'h', label: 'quota.vum_total'},
{id: 'vumUsed', key: 'i', label: 'quota.vum_used'},
{id: 'member', key: 'g', label: 'quota.member'},
],
// 测试报告列表
PERFORMANCE_REPORT_TABLE: [
{id: 'testName', key: 'a', label: 'report.test_name'},

View File

@ -2559,13 +2559,13 @@ export default {
},
quota: {
default: {
organization: "Organization Default Quota",
project: "Project Default Quota",
workspace: "Workspace Default Quota",
},
api: "Number of interface tests",
performance: "Number of performance tests",
resource_pool: "Available test resource pool",
max_threads: "Maximum Concurrency",
max_threads: "Same Period Maximum Concurrency",
duration: "Stress test duration(seconds)",
use_default: "Default Quota",
yes: "Yes",
@ -2576,6 +2576,10 @@ export default {
edit_quota_title: "{0} quota",
workspace_quota_list: "Workspace quota list of {0}",
unlimited: "Unlimited",
member: "Member",
project: "Project",
vum_total: "Total vum",
vum_used: "Used vum",
clean: "Clean"
},
schema: {
@ -2842,6 +2846,11 @@ export default {
read: "READ",
edit: "EDIT",
},
workspace_quota: {
name: "Quota",
read: "READ",
edit: "EDIT"
},
project_message: {
name: "Message",
read: "READ",

View File

@ -2563,13 +2563,13 @@ export default {
},
quota: {
default: {
organization: "组织默认配额",
project: "项目默认配额",
workspace: "工作空间默认配额",
},
api: "接口测试数量",
performance: "性能测试数量",
resource_pool: "可用测试资源池",
max_threads: "最大并发数",
max_threads: "同一时段最大并发数",
duration: "压测时长(秒)",
use_default: "使用默认配额",
yes: "是",
@ -2580,6 +2580,10 @@ export default {
edit_quota_title: "{0}的配额",
workspace_quota_list: "{0}的工作空间配额列表",
unlimited: "无限制",
member: "成员数",
project: "项目数",
vum_total: "总vum数量",
vum_used: "消耗vum数量",
clean: "清空"
},
schema: {
@ -2846,6 +2850,11 @@ export default {
read: "查询",
edit: "编辑"
},
workspace_quota: {
name: "配额管理",
read: "查询配额",
edit: "编辑"
},
project_message: {
name: "消息设置",
read: "查询",

View File

@ -2562,13 +2562,13 @@ export default {
},
quota: {
default: {
organization: "組織默認配額",
project: "項目默認配額",
workspace: "工作空間默認配額",
},
api: "接口測試數量",
performance: "性能測試數量",
resource_pool: "可用測試資源池",
max_threads: "最大並發數",
max_threads: "同一時段最大並發數",
duration: "壓測時長(秒)",
use_default: "使用默認配額",
yes: "是",
@ -2579,6 +2579,10 @@ export default {
edit_quota_title: "{0}的配額",
workspace_quota_list: "{0}的工作空間配額列表",
unlimited: "無限製",
member: "成員數",
project: "項目數",
vum_total: "總vum數量",
vum_used: "消耗vum數量",
clean: "清空"
},
schema: {
@ -2845,6 +2849,11 @@ export default {
read: "查詢",
edit: "編輯"
},
workspace_quota: {
name: "配額管理",
read: "查詢配額",
edit: "編輯"
},
project_message: {
name: "消息設置",
read: "查詢",