feat(backend): leaderboard tiers, N+1 fix, contributors, certificate approval, physical disable

- leaderboard: batch-fetch users (GetUsersByIDs), deterministic tie-break by user_id
- leaderboard: tier system (champion/gold/silver/bronze) based on rank percentile
- store: HasCompletedCurriculum requires cert.Status == "issued"; pending does not satisfy
- certificate: issueCertificateIfEligible creates pending row; admin approves via handleAdminIssueCertificate
- certificate: handleRequestPhysicalCertificate returns 410 Gone (feature disabled)
- settings: contributors CRUD (loadContributors, validateContributors, handleAdminContributorAvatarUpload)
- community: expose contributors in /api/community response
- rate_limit: separate DevMode bypass from redis-nil check in allowWSHandshake
- router: remove /api/search route; fix missing tab indent on /api/meta line
- migration 0032: contributors app_setting with seed entry
This commit is contained in:
ahp
2026-10-20 17:51:22 +03:30
parent 617d8ff680
commit 161610049c
9 changed files with 232 additions and 69 deletions
+4 -1
View File
@@ -282,8 +282,11 @@ func (s *Server) perUserRateLimit(limit int64, scope string, next http.HandlerFu
// allowWSHandshake enforces a per-IP budget on WebSocket upgrades.
// Returns false when the caller should receive 429 (already written by caller).
func (s *Server) allowWSHandshake(r *http.Request) bool {
if s.cfg.DevMode {
return true
}
if s.redis == nil {
return s.cfg.DevMode
return false
}
ip := clientAddress(r)
if ip == "" {