Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/openapi rate limit function #5267

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1b3e122
feat(portal): Add current limiting function to ConsumerToken
youngzil Nov 3, 2024
8b9d09b
Merge branch 'master' into feature/openapi-rate-limit-function
youngzil Nov 3, 2024
1bae71f
fix:add CHANGES.md and optimize some codes
youngzil Nov 3, 2024
a4c84f8
Merge branch 'master' into feature/openapi-rate-limit-function
youngzil Nov 10, 2024
826ea74
feat(openapi): 重构 ConsumerToken 限流功能
youngzil Nov 17, 2024
9e2c4e8
refactor(Consumer): Spelling error in attribute
youngzil Nov 17, 2024
ace5076
Merge branch 'master' into feature/openapi-rate-limit-function
youngzil Nov 20, 2024
a622f56
refactor(openapi): Refactor consumer authentication filters and relat…
youngzil Nov 21, 2024
4939d70
test(apollo-portal): Optimize the rate limiting test of ConsumerAuthe…
youngzil Nov 21, 2024
a9da81d
Merge branch 'master' into feature/openapi-rate-limit-function
youngzil Nov 23, 2024
22ebb4f
featapi(open): Updated management page to show consumer rate limit in…
youngzil Nov 23, 2024
edb9d8b
fix(portal): Optimized the robustness of the code
youngzil Nov 23, 2024
6011aeb
fix(portal): Optimized the robustness of the code
youngzil Nov 23, 2024
8463974
fix(portal): fix unit test
youngzil Nov 23, 2024
0fe67f5
Merge branch 'refs/heads/master' into feature/openapi-rate-limit-func…
youngzil Nov 26, 2024
5390f62
feat(openapi): Added consumer rate limit query function and optimized…
youngzil Nov 26, 2024
9126bb3
fix(portal): Fix the processing logic when the consumer obtains an em…
youngzil Nov 27, 2024
ddfd8b3
refactor(ConsumerService): Optimize the implementation of getRateLimi…
youngzil Nov 27, 2024
46df20c
Update CHANGES.md
nobodyiam Nov 28, 2024
e07de19
Update CHANGES.md
nobodyiam Nov 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public class ConsumerToken extends BaseEntity {
@Column(name = "`Token`", nullable = false)
private String token;

@Column(name = "LimitCount")
private Integer limitCount;

youngzil marked this conversation as resolved.
Show resolved Hide resolved
@Column(name = "`Expires`", nullable = false)
private Date expires;

Expand All @@ -60,6 +63,14 @@ public void setToken(String token) {
this.token = token;
}

public Integer getLimitCount() {
return limitCount;
}

public void setLimitCount(Integer limitCount) {
this.limitCount = limitCount;
}
youngzil marked this conversation as resolved.
Show resolved Hide resolved

public Date getExpires() {
return expires;
}
Expand All @@ -71,6 +82,7 @@ public void setExpires(Date expires) {
@Override
public String toString() {
return toStringHelper().add("consumerId", consumerId).add("token", token)
.add("limitCount", limitCount)
.add("expires", expires).toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@
*/
package com.ctrip.framework.apollo.openapi.filter;

import com.ctrip.framework.apollo.openapi.entity.ConsumerToken;
import com.ctrip.framework.apollo.openapi.util.ConsumerAuditUtil;
import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil;

import com.ctrip.framework.apollo.portal.component.config.PortalConfig;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.util.concurrent.RateLimiter;
import java.io.IOException;

import javax.servlet.Filter;
Expand All @@ -29,18 +33,29 @@
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;

/**
* @author Jason Song([email protected])
*/
public class ConsumerAuthenticationFilter implements Filter {

private static final Logger logger = LoggerFactory.getLogger(ConsumerAuthenticationFilter.class);

private final ConsumerAuthUtil consumerAuthUtil;
private final ConsumerAuditUtil consumerAuditUtil;
private final PortalConfig portalConfig;
youngzil marked this conversation as resolved.
Show resolved Hide resolved

public ConsumerAuthenticationFilter(ConsumerAuthUtil consumerAuthUtil, ConsumerAuditUtil consumerAuditUtil) {
private static final Cache<String, ImmutablePair<Long, RateLimiter>> LIMITER = CacheBuilder.newBuilder().build();
private static final int WARMUP_MILLIS = 1000; // ms
youngzil marked this conversation as resolved.
Show resolved Hide resolved

public ConsumerAuthenticationFilter(ConsumerAuthUtil consumerAuthUtil, ConsumerAuditUtil consumerAuditUtil, PortalConfig portalConfig) {
this.consumerAuthUtil = consumerAuthUtil;
this.consumerAuditUtil = consumerAuditUtil;
this.portalConfig = portalConfig;
}

@Override
Expand All @@ -55,14 +70,28 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain
HttpServletResponse response = (HttpServletResponse) resp;

String token = request.getHeader(HttpHeaders.AUTHORIZATION);
ConsumerToken consumerToken = consumerAuthUtil.getConsumerToken(token);

Long consumerId = consumerAuthUtil.getConsumerId(token);

if (consumerId == null) {
if (null == consumerToken || consumerToken.getConsumerId() <= 0) {
youngzil marked this conversation as resolved.
Show resolved Hide resolved
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
return;
}

Integer limitCount = consumerToken.getLimitCount();
if (portalConfig.isOpenApiLimitEnabled() && limitCount > 0) {
youngzil marked this conversation as resolved.
Show resolved Hide resolved
try {
ImmutablePair<Long, RateLimiter> rateLimiterPair = getOrCreateRateLimiterPair(token, limitCount);
long warmupToMillis = rateLimiterPair.getLeft() + WARMUP_MILLIS;
nobodyiam marked this conversation as resolved.
Show resolved Hide resolved
if (System.currentTimeMillis() > warmupToMillis && !rateLimiterPair.getRight().tryAcquire()) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Too many call requests, the flow is limited");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is 429 more suitable for this scenario?

return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential race condition in warmup check

The warmup check compares two timestamps without synchronization, which could lead to inconsistent behavior in high-concurrency scenarios. Consider using atomic operations or moving the warmup logic into the RateLimiter itself.

- long warmupToMillis = rateLimiterPair.getLeft() + WARMUP_MILLIS;
- if (System.currentTimeMillis() > warmupToMillis && !rateLimiterPair.getRight().tryAcquire()) {
+ RateLimiter limiter = rateLimiterPair.getRight();
+ if (!limiter.tryAcquire()) {
   response.sendError(HttpServletResponse.SC_FORBIDDEN, "Too many call requests, the flow is limited");
   return;
 }

Committable suggestion skipped: line range outside the PR's diff.

} catch (Exception e) {
logger.error("ConsumerAuthenticationFilter ratelimit error", e);
}
}
youngzil marked this conversation as resolved.
Show resolved Hide resolved

long consumerId = consumerToken.getConsumerId();
consumerAuthUtil.storeConsumerId(request, consumerId);
consumerAuditUtil.audit(request, consumerId);

Expand All @@ -73,4 +102,14 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain
public void destroy() {
//nothing
}

private ImmutablePair<Long, RateLimiter> getOrCreateRateLimiterPair(String key, Integer limitCount) {
ImmutablePair<Long, RateLimiter> rateLimiterPair = LIMITER.getIfPresent(key);
if (rateLimiterPair == null) {
rateLimiterPair = ImmutablePair.of(System.currentTimeMillis(), RateLimiter.create(limitCount));
LIMITER.put(key, rateLimiterPair);
}
return rateLimiterPair;
}
youngzil marked this conversation as resolved.
Show resolved Hide resolved

}
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,15 @@ public ConsumerToken getConsumerTokenByAppId(String appId) {
return consumerTokenRepository.findByConsumerId(consumer.getId());
}

public Long getConsumerIdByToken(String token) {
public ConsumerToken getConsumerTokenByToken(String token) {
if (Strings.isNullOrEmpty(token)) {
return null;
}
ConsumerToken consumerToken = consumerTokenRepository.findTopByTokenAndExpiresAfter(token,
new Date());
return consumerTokenRepository.findTopByTokenAndExpiresAfter(token, new Date());
}

public Long getConsumerIdByToken(String token) {
ConsumerToken consumerToken = getConsumerTokenByToken(token);
return consumerToken == null ? null : consumerToken.getConsumerId();
}

Expand Down Expand Up @@ -311,7 +314,9 @@ public void createConsumerAudits(Iterable<ConsumerAudit> consumerAudits) {
@Transactional
public ConsumerToken createConsumerToken(ConsumerToken entity) {
entity.setId(0); //for protection

if (entity.getLimitCount() <= 0) {
entity.setLimitCount(portalConfig.openApiLimitCount());
}
youngzil marked this conversation as resolved.
Show resolved Hide resolved
return consumerTokenRepository.save(entity);
}

Expand All @@ -322,6 +327,7 @@ private ConsumerToken generateConsumerToken(Consumer consumer, Date expires) {

ConsumerToken consumerToken = new ConsumerToken();
consumerToken.setConsumerId(consumerId);
consumerToken.setLimitCount(portalConfig.openApiLimitCount());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rate limit should be set on the UI page, with a default value that users can modify.

consumerToken.setExpires(expires);
consumerToken.setDataChangeCreatedBy(createdBy);
consumerToken.setDataChangeCreatedTime(createdTime);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package com.ctrip.framework.apollo.openapi.util;

import com.ctrip.framework.apollo.openapi.entity.ConsumerToken;
import com.ctrip.framework.apollo.openapi.service.ConsumerService;
import org.springframework.stereotype.Service;

Expand All @@ -37,6 +38,10 @@ public Long getConsumerId(String token) {
return consumerService.getConsumerIdByToken(token);
}

public ConsumerToken getConsumerToken(String token) {
return consumerService.getConsumerTokenByToken(token);
}
nobodyiam marked this conversation as resolved.
Show resolved Hide resolved

public void storeConsumerId(HttpServletRequest request, Long consumerId) {
request.setAttribute(CONSUMER_ID, consumerId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,14 @@ public String consumerTokenSalt() {
return getValue("consumer.token.salt", "apollo-portal");
}

public int openApiLimitCount() {
return getIntProperty("open.api.limit.count", 20);
}

public boolean isOpenApiLimitEnabled() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since rate limits are set at the token level, adding a global flag seems unnecessary. Instead, we could add an "Enable Rate Limit" option on the consumer token creation page. Users can toggle this option to configure the rate limit if desired; otherwise, leaving it unchecked will set the rate limit to 0.

return getBooleanProperty("open.api.limit.enabled", false);
}

public boolean isEmailEnabled() {
return getBooleanProperty("email.enabled", false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.ctrip.framework.apollo.openapi.filter.ConsumerAuthenticationFilter;
import com.ctrip.framework.apollo.openapi.util.ConsumerAuditUtil;
import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil;
import com.ctrip.framework.apollo.portal.component.config.PortalConfig;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -28,12 +29,13 @@ public class AuthFilterConfiguration {

@Bean
public FilterRegistrationBean<ConsumerAuthenticationFilter> openApiAuthenticationFilter(
ConsumerAuthUtil consumerAuthUtil,
ConsumerAuditUtil consumerAuditUtil) {
ConsumerAuthUtil consumerAuthUtil,
ConsumerAuditUtil consumerAuditUtil,
PortalConfig portalConfig) {

FilterRegistrationBean<ConsumerAuthenticationFilter> openApiFilter = new FilterRegistrationBean<>();

openApiFilter.setFilter(new ConsumerAuthenticationFilter(consumerAuthUtil, consumerAuditUtil));
openApiFilter.setFilter(new ConsumerAuthenticationFilter(consumerAuthUtil, consumerAuditUtil, portalConfig));
openApiFilter.addUrlPatterns("/openapi/*");

return openApiFilter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@
*/
package com.ctrip.framework.apollo.openapi.filter;

import com.ctrip.framework.apollo.openapi.entity.ConsumerToken;
import com.ctrip.framework.apollo.openapi.util.ConsumerAuditUtil;
import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil;

import com.ctrip.framework.apollo.portal.component.config.PortalConfig;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand All @@ -33,6 +40,9 @@
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
Expand All @@ -48,6 +58,9 @@ public class ConsumerAuthenticationFilterTest {
private ConsumerAuthUtil consumerAuthUtil;
@Mock
private ConsumerAuditUtil consumerAuditUtil;
@Mock
private PortalConfig portalConfig;

@Mock
private HttpServletRequest request;
@Mock
Expand All @@ -57,7 +70,7 @@ public class ConsumerAuthenticationFilterTest {

@Before
public void setUp() throws Exception {
authenticationFilter = new ConsumerAuthenticationFilter(consumerAuthUtil, consumerAuditUtil);
authenticationFilter = new ConsumerAuthenticationFilter(consumerAuthUtil, consumerAuditUtil, portalConfig);
}

@Test
Expand Down Expand Up @@ -89,4 +102,101 @@ public void testAuthFailed() throws Exception {
verify(consumerAuditUtil, never()).audit(eq(request), anyLong());
verify(filterChain, never()).doFilter(request, response);
}


@Test
public void testRateLimitSuccessfully() throws Exception {
String someToken = "someToken";
Long someConsumerId = 1L;
int qps = 5;
int durationInSeconds = 10;
youngzil marked this conversation as resolved.
Show resolved Hide resolved

setupRateLimitMocks(someToken, someConsumerId, qps);

Runnable task = () -> {
try {
authenticationFilter.doFilter(request, response, filterChain);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ServletException e) {
throw new RuntimeException(e);
}
};

executeWithQps(qps, task, durationInSeconds);

int total = qps * durationInSeconds;

verify(consumerAuthUtil, times(total)).storeConsumerId(request, someConsumerId);
verify(consumerAuditUtil, times(total)).audit(request, someConsumerId);
verify(filterChain, times(total)).doFilter(request, response);

}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve test determinism by refining timing control in rate limit tests

The testRateLimitSuccessfully method's reliance on TimeUnit.MILLISECONDS.sleep to control QPS can lead to non-deterministic test results due to thread scheduling and execution overhead. To ensure consistent and reliable test outcomes, consider mocking the rate limiter or using a ScheduledExecutorService for precise timing of task execution.



@Test
public void testRateLimitPartFailure() throws Exception {
String someToken = "someToken";
Long someConsumerId = 1L;
int qps = 5;
int durationInSeconds = 10;

setupRateLimitMocks(someToken, someConsumerId, qps);

Runnable task = () -> {
try {
authenticationFilter.doFilter(request, response, filterChain);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ServletException e) {
throw new RuntimeException(e);
}
};

executeWithQps(qps + 1, task, durationInSeconds);

int leastTimes = qps * durationInSeconds;
int mostTimes = (qps + 1) * durationInSeconds;

verify(response, atLeastOnce()).sendError(eq(HttpServletResponse.SC_FORBIDDEN), anyString());

verify(consumerAuthUtil, atLeast(leastTimes)).storeConsumerId(request, someConsumerId);
verify(consumerAuthUtil, atMost(mostTimes)).storeConsumerId(request, someConsumerId);
verify(consumerAuditUtil, atLeast(leastTimes)).audit(request, someConsumerId);
verify(consumerAuditUtil, atMost(mostTimes)).audit(request, someConsumerId);
verify(filterChain, atLeast(leastTimes)).doFilter(request, response);
verify(filterChain, atMost(mostTimes)).doFilter(request, response);

}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance reliability of rate limit failure test

Similar to testRateLimitSuccessfully, the testRateLimitPartFailure method may produce inconsistent results due to timing inaccuracies. To improve test reliability, consider mocking the rate limiter to simulate rate limit breaches or refactoring the test to reduce dependency on precise timing control.



private void setupRateLimitMocks(String someToken, Long someConsumerId, int qps) {
ConsumerToken someConsumerToken = new ConsumerToken();
someConsumerToken.setConsumerId(someConsumerId);
someConsumerToken.setLimitCount(qps);

when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(someToken);
when(consumerAuthUtil.getConsumerId(someToken)).thenReturn(someConsumerId);
when(consumerAuthUtil.getConsumerToken(someToken)).thenReturn(someConsumerToken);
when(portalConfig.isOpenApiLimitEnabled()).thenReturn(true);
}


public static void executeWithQps(int qps, Runnable task, int durationInSeconds) {
ExecutorService executor = Executors.newFixedThreadPool(qps);
long totalTasks = qps * durationInSeconds;

for (int i = 0; i < totalTasks; i++) {
executor.submit(task);
try {
TimeUnit.MILLISECONDS.sleep(1000 / qps); // Control QPS
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}

executor.shutdown();
}
Comment on lines +189 to +204
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Enhance executeWithQps reliability and resource management.

The current implementation has several potential issues:

  1. No proper verification of task completion
  2. Potential thread pool resource leak
  3. No shutdown timeout handling

Consider this improved implementation:

 public static void executeWithQps(int qps, Runnable task, int durationInSeconds) {
   ExecutorService executor = Executors.newFixedThreadPool(qps);
   long totalTasks = qps * durationInSeconds;
+  try {
     for (int i = 0; i < totalTasks; i++) {
-      executor.submit(task);
+      Future<?> future = executor.submit(task);
       try {
+        future.get(1000 / qps, TimeUnit.MILLISECONDS); // Ensure task completes
         TimeUnit.MILLISECONDS.sleep(1000 / qps); // Control QPS
       } catch (InterruptedException e) {
         Thread.currentThread().interrupt();
         break;
+      } catch (TimeoutException | ExecutionException e) {
+        throw new RuntimeException("Task execution failed", e);
       }
     }
+  } finally {
     executor.shutdown();
+    try {
+      if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
+        executor.shutdownNow();
+      }
+    } catch (InterruptedException e) {
+      executor.shutdownNow();
+      Thread.currentThread().interrupt();
+    }
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static void executeWithQps(int qps, Runnable task, int durationInSeconds) {
ExecutorService executor = Executors.newFixedThreadPool(qps);
long totalTasks = qps * durationInSeconds;
for (int i = 0; i < totalTasks; i++) {
executor.submit(task);
try {
TimeUnit.MILLISECONDS.sleep(1000 / qps); // Control QPS
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
executor.shutdown();
}
public static void executeWithQps(int qps, Runnable task, int durationInSeconds) {
ExecutorService executor = Executors.newFixedThreadPool(qps);
long totalTasks = qps * durationInSeconds;
try {
for (int i = 0; i < totalTasks; i++) {
Future<?> future = executor.submit(task);
try {
future.get(1000 / qps, TimeUnit.MILLISECONDS); // Ensure task completes
TimeUnit.MILLISECONDS.sleep(1000 / qps); // Control QPS
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (TimeoutException | ExecutionException e) {
throw new RuntimeException("Task execution failed", e);
}
}
} finally {
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}


}
1 change: 1 addition & 0 deletions scripts/sql/profiles/h2-default/apolloportaldb.sql
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ CREATE TABLE `ConsumerToken` (
`Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id',
`ConsumerId` int(11) unsigned DEFAULT NULL COMMENT 'ConsumerId',
`Token` varchar(128) NOT NULL DEFAULT '' COMMENT 'token',
`LimitCount` int NOT NULL DEFAULT 20 COMMENT '限流值',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider documenting rate limit implementation details.

The new LimitCount column is added with a default value of 20, but there are some considerations:

  1. The comment '限流值' (Rate limit value) should include more details about:
    • The time window for the rate limit (e.g., requests per second/minute)
    • The rationale behind the default value of 20
  2. Consider adding a maximum value constraint to prevent unreasonable limits

Apply this diff to enhance the column definition:

-  `LimitCount` int NOT NULL DEFAULT 20 COMMENT '限流值',
+  `LimitCount` int NOT NULL DEFAULT 20 CHECK (`LimitCount` BETWEEN 1 AND 1000) COMMENT 'Rate limit value (requests per minute). Default: 20 req/min. Range: 1-1000.',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`LimitCount` int NOT NULL DEFAULT 20 COMMENT '限流值',
`LimitCount` int NOT NULL DEFAULT 20 CHECK (`LimitCount` BETWEEN 1 AND 1000) COMMENT 'Rate limit value (requests per minute). Default: 20 req/min. Range: 1-1000.',

`Expires` datetime NOT NULL DEFAULT '2099-01-01 00:00:00' COMMENT 'token失效时间',
`IsDeleted` boolean NOT NULL DEFAULT FALSE COMMENT '1: deleted, 0: normal',
`DeletedAt` BIGINT(20) NOT NULL DEFAULT '0' COMMENT 'Delete timestamp based on milliseconds',
Expand Down
Loading
Loading