实现叫号播报并调整现场业务规则
需求描述:发布屏在员工叫号后播放语音;同场次手机号被叫号后可重新取号;登录连续失败限制调整为10次。 实现思路:发布屏按新批次去重触发中文语音;重复手机号仅拦截WAITING号码;登录限流使用10次阈值,并补充前后端回归测试与接口说明。
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const loginFailureLimit = 10
|
||||
|
||||
type loginAttempt struct {
|
||||
failures int
|
||||
windowStart time.Time
|
||||
@@ -93,7 +95,7 @@ func (l *loginLimiter) failure(key string) {
|
||||
attempt = loginAttempt{windowStart: now}
|
||||
}
|
||||
attempt.failures++
|
||||
if attempt.failures >= 5 {
|
||||
if attempt.failures >= loginFailureLimit {
|
||||
attempt.blockedTill = now.Add(10 * time.Minute)
|
||||
}
|
||||
l.attempts[key] = attempt
|
||||
|
||||
@@ -24,3 +24,21 @@ func TestQueryLimiterResetsAfterWindow(t *testing.T) {
|
||||
t.Fatal("query should be allowed after the window resets")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginLimiterBlocksAfterTenFailures(t *testing.T) {
|
||||
now := time.Unix(100, 0)
|
||||
limiter := newLoginLimiter(func() time.Time { return now })
|
||||
const key = "staff|127.0.0.1"
|
||||
|
||||
for attempt := 1; attempt < 10; attempt++ {
|
||||
limiter.failure(key)
|
||||
if allowed, _ := limiter.allow(key); !allowed {
|
||||
t.Fatalf("login should remain allowed after %d failures", attempt)
|
||||
}
|
||||
}
|
||||
|
||||
limiter.failure(key)
|
||||
if allowed, retry := limiter.allow(key); allowed || retry != 10*time.Minute {
|
||||
t.Fatalf("login should be locked for 10 minutes after 10 failures, got allowed=%v retry=%s", allowed, retry)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,29 @@ func TestPublicCreateTicketIgnoresDuplicateFromEndedSession(t *testing.T) {
|
||||
t.Fatalf("current-session duplicate status = %d, want 409 DUPLICATE_PHONE; body = %s",
|
||||
duplicateRecorder.Code, duplicateRecorder.Body.String())
|
||||
}
|
||||
|
||||
calledAt := now
|
||||
result := db.Model(&model.QueueTicket{}).
|
||||
Where("project_id = ? AND queue_session_id = ? AND phone_hmac = ? AND status = ?",
|
||||
projectID, currentSession.ID, phoneHMAC, model.TicketWaiting).
|
||||
Updates(map[string]any{"status": model.TicketCalled, "called_at": calledAt, "updated_at": calledAt})
|
||||
if result.Error != nil {
|
||||
t.Fatal(result.Error)
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
t.Fatalf("called tickets = %d, want 1", result.RowsAffected)
|
||||
}
|
||||
|
||||
afterCallRecorder := httptest.NewRecorder()
|
||||
afterCallRequest := httptest.NewRequest(http.MethodPost, "/api/public/projects/"+projectID+"/tickets",
|
||||
strings.NewReader(`{"phone":"13800138000","honorific":"游客","party_size":1,"allow_duplicate":false}`))
|
||||
afterCallRequest.Header.Set("Idempotency-Key", "public-after-call-"+uuid.NewString())
|
||||
|
||||
server.Handler().ServeHTTP(afterCallRecorder, afterCallRequest)
|
||||
|
||||
if afterCallRecorder.Code != http.StatusCreated {
|
||||
t.Fatalf("after-call status = %d, want 201; body = %s", afterCallRecorder.Code, afterCallRecorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternalPhoneLookupUsesLatestActiveSessionPerProject(t *testing.T) {
|
||||
|
||||
@@ -296,8 +296,8 @@ func (s *Server) createTicketForActor(w http.ResponseWriter, r *http.Request, ac
|
||||
phoneDigest := s.cipher.Digest(phone)
|
||||
var duplicates []model.QueueTicket
|
||||
if err := tx.Select("id", "display_number", "status").
|
||||
Where("project_id = ? AND queue_session_id = ? AND phone_hmac = ? AND status IN ?", projectID, session.ID, phoneDigest,
|
||||
[]string{model.TicketWaiting, model.TicketCalled, model.TicketArrived}).
|
||||
Where("project_id = ? AND queue_session_id = ? AND phone_hmac = ? AND status = ?",
|
||||
projectID, session.ID, phoneDigest, model.TicketWaiting).
|
||||
Order("joined_at ASC").Find(&duplicates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user