i18n: Translate all Chinese to English across the codebase
Translate Chinese comments, log/error messages, validator labels and Swagger annotations to English throughout the source code, generated Swagger docs, config files and CI workflows. Make the English README primary: README.md now holds the English docs and README_EN.md holds the Chinese version, with cross-language links updated accordingly. Note: the generated docs/ swagger files were translated in place; run `go generate ./...` (swag init) to regenerate them from the now-English annotations when a Go toolchain is available. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+33
-33
@@ -6,23 +6,23 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// 安全策略配置
|
||||
// security policy configuration
|
||||
type SecurityPolicy struct {
|
||||
CaptchaThreshold int // 尝试失败次数达到验证码阈值,小于0表示不启用, 0表示强制启用
|
||||
BanThreshold int // 尝试失败次数达到封禁阈值,为0表示不启用
|
||||
CaptchaThreshold int // number of failed attempts at which the captcha is required; less than 0 means disabled, 0 means always enabled
|
||||
BanThreshold int // number of failed attempts at which a ban is triggered; 0 means disabled
|
||||
AttemptsWindow time.Duration
|
||||
BanDuration time.Duration
|
||||
}
|
||||
|
||||
// 验证码提供者接口
|
||||
// captcha provider interface
|
||||
type CaptchaProvider interface {
|
||||
Generate() (id string, content string, answer string, err error)
|
||||
//Validate(ip, code string) bool
|
||||
Expiration() time.Duration // 验证码过期时间, 应该小于 AttemptsWindow
|
||||
Draw(content string) (string, error) // 绘制验证码
|
||||
Expiration() time.Duration // captcha expiration time, should be less than AttemptsWindow
|
||||
Draw(content string) (string, error) // draw the captcha
|
||||
}
|
||||
|
||||
// 验证码元数据
|
||||
// captcha metadata
|
||||
type CaptchaMeta struct {
|
||||
Id string
|
||||
Content string
|
||||
@@ -30,13 +30,13 @@ type CaptchaMeta struct {
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// IP封禁记录
|
||||
// IP ban record
|
||||
type BanRecord struct {
|
||||
ExpiresAt time.Time
|
||||
Reason string
|
||||
}
|
||||
|
||||
// 登录限制器
|
||||
// login limiter
|
||||
type LoginLimiter struct {
|
||||
mu sync.Mutex
|
||||
policy SecurityPolicy
|
||||
@@ -55,7 +55,7 @@ var defaultSecurityPolicy = SecurityPolicy{
|
||||
}
|
||||
|
||||
func NewLoginLimiter(policy SecurityPolicy) *LoginLimiter {
|
||||
// 设置默认值
|
||||
// set default values
|
||||
if policy.AttemptsWindow == 0 {
|
||||
policy.AttemptsWindow = 5 * time.Minute
|
||||
}
|
||||
@@ -74,19 +74,19 @@ func NewLoginLimiter(policy SecurityPolicy) *LoginLimiter {
|
||||
return ll
|
||||
}
|
||||
|
||||
// 注册验证码提供者
|
||||
// register a captcha provider
|
||||
func (ll *LoginLimiter) RegisterProvider(p CaptchaProvider) {
|
||||
ll.mu.Lock()
|
||||
defer ll.mu.Unlock()
|
||||
ll.provider = p
|
||||
}
|
||||
|
||||
// isDisabled 检查是否禁用登录限制
|
||||
// isDisabled checks whether the login limiter is disabled
|
||||
func (ll *LoginLimiter) isDisabled() bool {
|
||||
return ll.policy.CaptchaThreshold < 0 && ll.policy.BanThreshold == 0
|
||||
}
|
||||
|
||||
// 记录登录失败尝试
|
||||
// record a failed login attempt
|
||||
func (ll *LoginLimiter) RecordFailedAttempt(ip string) {
|
||||
if ll.isDisabled() {
|
||||
return
|
||||
@@ -101,14 +101,14 @@ func (ll *LoginLimiter) RecordFailedAttempt(ip string) {
|
||||
now := time.Now()
|
||||
windowStart := now.Add(-ll.policy.AttemptsWindow)
|
||||
|
||||
// 清理过期尝试
|
||||
// clean up expired attempts
|
||||
validAttempts := ll.pruneAttempts(ip, windowStart)
|
||||
|
||||
// 记录新尝试
|
||||
// record a new attempt
|
||||
validAttempts = append(validAttempts, now)
|
||||
ll.attempts[ip] = validAttempts
|
||||
|
||||
// 检查封禁条件
|
||||
// check ban conditions
|
||||
if ll.policy.BanThreshold > 0 && len(validAttempts) >= ll.policy.BanThreshold {
|
||||
ll.banIP(ip, "excessive failed attempts")
|
||||
return
|
||||
@@ -117,7 +117,7 @@ func (ll *LoginLimiter) RecordFailedAttempt(ip string) {
|
||||
return
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
// generate a captcha
|
||||
func (ll *LoginLimiter) RequireCaptcha() (error, CaptchaMeta) {
|
||||
ll.mu.Lock()
|
||||
defer ll.mu.Unlock()
|
||||
@@ -131,7 +131,7 @@ func (ll *LoginLimiter) RequireCaptcha() (error, CaptchaMeta) {
|
||||
return err, CaptchaMeta{}
|
||||
}
|
||||
|
||||
// 存储验证码
|
||||
// store the captcha
|
||||
ll.captchas[id] = CaptchaMeta{
|
||||
Id: id,
|
||||
Content: content,
|
||||
@@ -142,29 +142,29 @@ func (ll *LoginLimiter) RequireCaptcha() (error, CaptchaMeta) {
|
||||
return nil, ll.captchas[id]
|
||||
}
|
||||
|
||||
// 验证验证码
|
||||
// verify the captcha
|
||||
func (ll *LoginLimiter) VerifyCaptcha(id, answer string) bool {
|
||||
ll.mu.Lock()
|
||||
defer ll.mu.Unlock()
|
||||
|
||||
// 查找匹配验证码
|
||||
// find a matching captcha
|
||||
if ll.provider == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取并验证验证码
|
||||
// get and verify the captcha
|
||||
captcha, exists := ll.captchas[id]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
// 清理过期验证码
|
||||
// clean up expired captchas
|
||||
if time.Now().After(captcha.ExpiresAt) {
|
||||
delete(ll.captchas, id)
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证并清理状态
|
||||
// verify and clean up state
|
||||
if answer == captcha.Answer {
|
||||
delete(ll.captchas, id)
|
||||
return true
|
||||
@@ -178,7 +178,7 @@ func (ll *LoginLimiter) DrawCaptcha(content string) (err error, str string) {
|
||||
return
|
||||
}
|
||||
|
||||
// 清除记录窗口
|
||||
// clear the record window
|
||||
func (ll *LoginLimiter) RemoveAttempts(ip string) {
|
||||
ll.mu.Lock()
|
||||
defer ll.mu.Unlock()
|
||||
@@ -189,7 +189,7 @@ func (ll *LoginLimiter) RemoveAttempts(ip string) {
|
||||
}
|
||||
}
|
||||
|
||||
// CheckSecurityStatus 检查安全状态
|
||||
// CheckSecurityStatus checks the security status
|
||||
func (ll *LoginLimiter) CheckSecurityStatus(ip string) (banned bool, captchaRequired bool) {
|
||||
if ll.isDisabled() {
|
||||
return
|
||||
@@ -197,21 +197,21 @@ func (ll *LoginLimiter) CheckSecurityStatus(ip string) (banned bool, captchaRequ
|
||||
ll.mu.Lock()
|
||||
defer ll.mu.Unlock()
|
||||
|
||||
// 检查封禁状态
|
||||
// check ban status
|
||||
if banned, _ = ll.isBanned(ip); banned {
|
||||
return
|
||||
}
|
||||
|
||||
// 清理过期数据
|
||||
// clean up expired data
|
||||
ll.pruneAttempts(ip, time.Now().Add(-ll.policy.AttemptsWindow))
|
||||
|
||||
// 检查验证码要求
|
||||
// check captcha requirement
|
||||
captchaRequired = len(ll.attempts[ip]) >= ll.policy.CaptchaThreshold
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 后台清理任务
|
||||
// background cleanup task
|
||||
func (ll *LoginLimiter) cleanupRoutine() {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
@@ -226,7 +226,7 @@ func (ll *LoginLimiter) cleanupRoutine() {
|
||||
}
|
||||
}
|
||||
|
||||
// 内部工具方法
|
||||
// internal utility methods
|
||||
func (ll *LoginLimiter) isBanned(ip string) (bool, BanRecord) {
|
||||
record, exists := ll.bannedIPs[ip]
|
||||
if !exists {
|
||||
@@ -277,19 +277,19 @@ func (ll *LoginLimiter) cleanupExpired() {
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// 清理封禁记录
|
||||
// clean up ban records
|
||||
for ip, record := range ll.bannedIPs {
|
||||
if now.After(record.ExpiresAt) {
|
||||
delete(ll.bannedIPs, ip)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理尝试记录
|
||||
// clean up attempt records
|
||||
for ip := range ll.attempts {
|
||||
ll.pruneAttempts(ip, now.Add(-ll.policy.AttemptsWindow))
|
||||
}
|
||||
|
||||
// 清理验证码
|
||||
// clean up captchas
|
||||
for id := range ll.captchas {
|
||||
ll.pruneCaptchas(id)
|
||||
}
|
||||
|
||||
+62
-62
@@ -33,7 +33,7 @@ func TestSecurityWorkflow(t *testing.T) {
|
||||
limiter := NewLoginLimiter(policy)
|
||||
ip := "192.168.1.100"
|
||||
|
||||
// 测试正常失败记录
|
||||
// test normal failure recording
|
||||
for i := 0; i < 3; i++ {
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
}
|
||||
@@ -45,14 +45,14 @@ func TestSecurityWorkflow(t *testing.T) {
|
||||
if !capRequired {
|
||||
t.Error("Captcha should be required")
|
||||
}
|
||||
// 测试触发封禁
|
||||
// test triggering a ban
|
||||
for i := 0; i < 3; i++ {
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
isBanned, capRequired = limiter.CheckSecurityStatus(ip)
|
||||
fmt.Printf("IP: %s, Banned: %v, Captcha Required: %v\n", ip, isBanned, capRequired)
|
||||
}
|
||||
|
||||
// 测试封禁状态
|
||||
// test ban status
|
||||
if isBanned, _ = limiter.CheckSecurityStatus(ip); !isBanned {
|
||||
t.Error("IP should be banned")
|
||||
}
|
||||
@@ -64,36 +64,36 @@ func TestCaptchaFlow(t *testing.T) {
|
||||
limiter.RegisterProvider(&MockCaptchaProvider{})
|
||||
ip := "10.0.0.1"
|
||||
|
||||
// 触发验证码要求
|
||||
// trigger captcha requirement
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
|
||||
// 检查状态
|
||||
// check status
|
||||
if _, need := limiter.CheckSecurityStatus(ip); !need {
|
||||
t.Error("应该需要验证码")
|
||||
t.Error("captcha should be required")
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
// generate captcha
|
||||
err, capc := limiter.RequireCaptcha()
|
||||
if err != nil {
|
||||
t.Fatalf("生成验证码失败: %v", err)
|
||||
t.Fatalf("failed to generate captcha: %v", err)
|
||||
}
|
||||
fmt.Printf("验证码内容: %#v\n", capc)
|
||||
fmt.Printf("captcha content: %#v\n", capc)
|
||||
|
||||
// 验证成功
|
||||
// verify successfully
|
||||
if !limiter.VerifyCaptcha(capc.Id, capc.Answer) {
|
||||
t.Error("验证码应该验证成功")
|
||||
t.Error("captcha should verify successfully")
|
||||
}
|
||||
|
||||
// 验证已删除
|
||||
// verify it has been deleted
|
||||
if limiter.VerifyCaptcha(capc.Id, capc.Answer) {
|
||||
t.Error("验证码应该已删除")
|
||||
t.Error("captcha should have been deleted")
|
||||
}
|
||||
|
||||
limiter.RemoveAttempts(ip)
|
||||
// 验证后状态
|
||||
// state after verification
|
||||
if banned, need := limiter.CheckSecurityStatus(ip); banned || need {
|
||||
t.Error("验证成功后应该重置状态")
|
||||
t.Error("state should be reset after successful verification")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,26 +103,26 @@ func TestCaptchaMustFlow(t *testing.T) {
|
||||
limiter.RegisterProvider(&MockCaptchaProvider{})
|
||||
ip := "10.0.0.1"
|
||||
|
||||
// 检查状态
|
||||
// check status
|
||||
if _, need := limiter.CheckSecurityStatus(ip); !need {
|
||||
t.Error("应该需要验证码")
|
||||
t.Error("captcha should be required")
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
// generate captcha
|
||||
err, capc := limiter.RequireCaptcha()
|
||||
if err != nil {
|
||||
t.Fatalf("生成验证码失败: %v", err)
|
||||
t.Fatalf("failed to generate captcha: %v", err)
|
||||
}
|
||||
fmt.Printf("验证码内容: %#v\n", capc)
|
||||
fmt.Printf("captcha content: %#v\n", capc)
|
||||
|
||||
// 验证成功
|
||||
// verify successfully
|
||||
if !limiter.VerifyCaptcha(capc.Id, capc.Answer) {
|
||||
t.Error("验证码应该验证成功")
|
||||
t.Error("captcha should verify successfully")
|
||||
}
|
||||
|
||||
// 验证后状态
|
||||
// state after verification
|
||||
if _, need := limiter.CheckSecurityStatus(ip); !need {
|
||||
t.Error("应该需要验证码")
|
||||
t.Error("captcha should be required")
|
||||
}
|
||||
}
|
||||
func TestAttemptTimeout(t *testing.T) {
|
||||
@@ -131,28 +131,28 @@ func TestAttemptTimeout(t *testing.T) {
|
||||
limiter.RegisterProvider(&MockCaptchaProvider{})
|
||||
ip := "10.0.0.1"
|
||||
|
||||
// 触发验证码要求
|
||||
// trigger captcha requirement
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
|
||||
// 检查状态
|
||||
// check status
|
||||
if _, need := limiter.CheckSecurityStatus(ip); !need {
|
||||
t.Error("应该需要验证码")
|
||||
t.Error("captcha should be required")
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
// generate captcha
|
||||
err, _ := limiter.RequireCaptcha()
|
||||
if err != nil {
|
||||
t.Fatalf("生成验证码失败: %v", err)
|
||||
t.Fatalf("failed to generate captcha: %v", err)
|
||||
}
|
||||
// 等待超过 AttemptsWindow
|
||||
// wait beyond AttemptsWindow
|
||||
time.Sleep(2 * time.Second)
|
||||
// 触发验证码要求
|
||||
// trigger captcha requirement
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
|
||||
// 检查状态
|
||||
// check status
|
||||
if _, need := limiter.CheckSecurityStatus(ip); need {
|
||||
t.Error("不应该需要验证码")
|
||||
t.Error("captcha should not be required")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,27 +162,27 @@ func TestCaptchaTimeout(t *testing.T) {
|
||||
limiter.RegisterProvider(&MockCaptchaProvider{})
|
||||
ip := "10.0.0.1"
|
||||
|
||||
// 触发验证码要求
|
||||
// trigger captcha requirement
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
|
||||
// 检查状态
|
||||
// check status
|
||||
if _, need := limiter.CheckSecurityStatus(ip); !need {
|
||||
t.Error("应该需要验证码")
|
||||
t.Error("captcha should be required")
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
// generate captcha
|
||||
err, capc := limiter.RequireCaptcha()
|
||||
if err != nil {
|
||||
t.Fatalf("生成验证码失败: %v", err)
|
||||
t.Fatalf("failed to generate captcha: %v", err)
|
||||
}
|
||||
|
||||
// 等待超过 CaptchaValidPeriod
|
||||
// wait beyond CaptchaValidPeriod
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// 验证成功
|
||||
// verify successfully
|
||||
if limiter.VerifyCaptcha(capc.Id, capc.Answer) {
|
||||
t.Error("验证码应该已过期")
|
||||
t.Error("captcha should have expired")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -191,12 +191,12 @@ func TestBanFlow(t *testing.T) {
|
||||
policy := SecurityPolicy{BanThreshold: 5}
|
||||
limiter := NewLoginLimiter(policy)
|
||||
ip := "10.0.0.1"
|
||||
// 触发ban
|
||||
// trigger ban
|
||||
for i := 0; i < 5; i++ {
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
// check status
|
||||
if banned, _ := limiter.CheckSecurityStatus(ip); !banned {
|
||||
t.Error("should be banned")
|
||||
}
|
||||
@@ -205,12 +205,12 @@ func TestBanDisableFlow(t *testing.T) {
|
||||
policy := SecurityPolicy{BanThreshold: 0}
|
||||
limiter := NewLoginLimiter(policy)
|
||||
ip := "10.0.0.1"
|
||||
// 触发ban
|
||||
// trigger ban
|
||||
for i := 0; i < 5; i++ {
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
// check status
|
||||
if banned, _ := limiter.CheckSecurityStatus(ip); banned {
|
||||
t.Error("should not be banned")
|
||||
}
|
||||
@@ -219,15 +219,15 @@ func TestBanTimeout(t *testing.T) {
|
||||
policy := SecurityPolicy{BanThreshold: 5, BanDuration: 1 * time.Second}
|
||||
limiter := NewLoginLimiter(policy)
|
||||
ip := "10.0.0.1"
|
||||
// 触发ban
|
||||
// 触发ban
|
||||
// trigger ban
|
||||
// trigger ban
|
||||
for i := 0; i < 5; i++ {
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// 检查状态
|
||||
// check status
|
||||
if banned, _ := limiter.CheckSecurityStatus(ip); banned {
|
||||
t.Error("should not be banned")
|
||||
}
|
||||
@@ -237,12 +237,12 @@ func TestLimiterDisabled(t *testing.T) {
|
||||
policy := SecurityPolicy{BanThreshold: 0, CaptchaThreshold: -1}
|
||||
limiter := NewLoginLimiter(policy)
|
||||
ip := "10.0.0.1"
|
||||
// 触发ban
|
||||
// trigger ban
|
||||
for i := 0; i < 5; i++ {
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
// check status
|
||||
if banned, capNeed := limiter.CheckSecurityStatus(ip); banned || capNeed {
|
||||
fmt.Printf("IP: %s, Banned: %v, Captcha Required: %v\n", ip, banned, capNeed)
|
||||
t.Error("should not be banned or need captcha")
|
||||
@@ -254,37 +254,37 @@ func TestB64CaptchaFlow(t *testing.T) {
|
||||
limiter.RegisterProvider(B64StringCaptchaProvider{})
|
||||
ip := "10.0.0.1"
|
||||
|
||||
// 触发验证码要求
|
||||
// trigger captcha requirement
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
limiter.RecordFailedAttempt(ip)
|
||||
|
||||
// 检查状态
|
||||
// check status
|
||||
if _, need := limiter.CheckSecurityStatus(ip); !need {
|
||||
t.Error("应该需要验证码")
|
||||
t.Error("captcha should be required")
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
// generate captcha
|
||||
err, capc := limiter.RequireCaptcha()
|
||||
if err != nil {
|
||||
t.Fatalf("生成验证码失败: %v", err)
|
||||
t.Fatalf("failed to generate captcha: %v", err)
|
||||
}
|
||||
fmt.Printf("验证码内容: %#v\n", capc)
|
||||
fmt.Printf("captcha content: %#v\n", capc)
|
||||
|
||||
//draw
|
||||
err, b64 := limiter.DrawCaptcha(capc.Content)
|
||||
if err != nil {
|
||||
t.Fatalf("绘制验证码失败: %v", err)
|
||||
t.Fatalf("failed to draw captcha: %v", err)
|
||||
}
|
||||
fmt.Printf("验证码内容: %#v\n", b64)
|
||||
fmt.Printf("captcha content: %#v\n", b64)
|
||||
|
||||
// 验证成功
|
||||
// verify successfully
|
||||
if !limiter.VerifyCaptcha(capc.Id, capc.Answer) {
|
||||
t.Error("验证码应该验证成功")
|
||||
t.Error("captcha should verify successfully")
|
||||
}
|
||||
limiter.RemoveAttempts(ip)
|
||||
// 验证后状态
|
||||
// state after verification
|
||||
if banned, need := limiter.CheckSecurityStatus(ip); banned || need {
|
||||
t.Error("验证成功后应该重置状态")
|
||||
t.Error("state should be reset after successful verification")
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -23,7 +23,7 @@ func CopyStructByJson(src, dst interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// CopyStructToMap 结构体转map
|
||||
// CopyStructToMap converts a struct to a map
|
||||
func CopyStructToMap(src interface{}) map[string]interface{} {
|
||||
var res = map[string]interface{}{}
|
||||
str, _ := json.Marshal(src)
|
||||
@@ -64,7 +64,7 @@ func SafeGo(f interface{}, params ...interface{}) {
|
||||
}()
|
||||
}
|
||||
|
||||
// RandomString 生成随机字符串
|
||||
// RandomString generates a random string
|
||||
func RandomString(n int) string {
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
length := len(letterBytes)
|
||||
@@ -79,7 +79,7 @@ func RandomString(n int) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Keys 泛型函数,K 是键类型,V 是值类型
|
||||
// Keys generic function; K is the key type, V is the value type
|
||||
func Keys[K comparable, V any](m map[K]V) []K {
|
||||
keys := make([]K, 0, len(m))
|
||||
for k := range m {
|
||||
@@ -88,7 +88,7 @@ func Keys[K comparable, V any](m map[K]V) []K {
|
||||
return keys
|
||||
}
|
||||
|
||||
// Values 泛型函数,K 是键类型,V 是值类型
|
||||
// Values generic function; K is the key type, V is the value type
|
||||
func Values[K comparable, V any](m map[K]V) []V {
|
||||
values := make([]V, 0, len(m))
|
||||
for _, v := range m {
|
||||
|
||||
Reference in New Issue
Block a user