Микросервис обучения портала: тесты, курсы, видео-уроки, доступы, public-ссылки для кандидатов с email-валидацией. В этой итерации: - Skeleton (config, migrate, main, health) по паттерну tasks/candidates - Migration 001_init: 10 таблиц (tests/questions/answers/attempts/ attempt_answers + courses/lessons/lesson_progress + access_grants + public_tokens) с подробными комментариями why - Tests: полный CRUD + вопросы/ответы; non-owner'у is_correct и explanation скрываются в выдаче - Заглушки 501 для attempts / courses / lessons / video-stream / access / public-tokens — следующие итерации - k8s: namespace, configmap, secrets, postgres, deployment с HPA, service с portal-discovery annotations - Dockerfile, Makefile, .gitignore См. README.md для полного списка отложенного и инструкций запуска. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
98 lines
3.2 KiB
Go
98 lines
3.2 KiB
Go
// Package config — env-based конфиг learning-сервиса. Стиль envStr идентичен
|
||
// candidates/tasks/booking — чтобы k8s-манифесты выглядели единообразно.
|
||
package config
|
||
|
||
import (
|
||
"os"
|
||
"strconv"
|
||
)
|
||
|
||
type Config struct {
|
||
ServerPort string
|
||
DatabaseURL string
|
||
MigrationsDir string
|
||
|
||
// InternalAPIKey — общий ключ с порталом для server-to-server auth.
|
||
// Portal-proxy ставит X-Internal-Key + X-User-Id; сервис верифицирует
|
||
// ключ и доверяет X-User-Id (JWT уже провалидирован на портале).
|
||
InternalAPIKey string
|
||
|
||
// PortalURL — для in-app уведомлений (назначен курс/тест/пройден тест).
|
||
// Если пусто — notify-вызовы тихо no-op'ятся, основной flow работает.
|
||
PortalURL string
|
||
|
||
// PublicBaseURL — внешний origin, под которым отдаются public-ссылки
|
||
// для кандидатов (например, https://portal.estateliga.work). Используется
|
||
// в формате URL'ей: <PUBLIC_BASE>/public/learning/test/<token>.
|
||
PublicBaseURL string
|
||
|
||
// MinIO для видео-уроков. Структура та же что в telephony (records bucket),
|
||
// но bucket отдельный — public-link'и читают видео через стрим-прокси.
|
||
MinIOEndpoint string
|
||
MinIOAccessKey string
|
||
MinIOSecretKey string
|
||
MinIOBucket string
|
||
MinIOUseSSL bool
|
||
|
||
// RedisAddr — пустой = eventbus отключён, сервис работает без публикации
|
||
// событий (попадание кандидата на тест не уведомит HR в realtime).
|
||
RedisAddr string
|
||
RedisPassword string
|
||
RedisDB int
|
||
|
||
PodName string
|
||
}
|
||
|
||
func Load() *Config {
|
||
return &Config{
|
||
ServerPort: envStr("SERVER_PORT", "3001"),
|
||
DatabaseURL: envStr("DATABASE_URL", "postgres://learning:learning@localhost:5432/learning?sslmode=disable"),
|
||
MigrationsDir: envStr("MIGRATIONS_DIR", "/migrations"),
|
||
InternalAPIKey: envStr("INTERNAL_API_KEY", envStr("PORTAL_INTERNAL_API_KEY", "")),
|
||
PortalURL: envStr("PORTAL_URL", ""),
|
||
PublicBaseURL: envStr("PUBLIC_BASE_URL", ""),
|
||
MinIOEndpoint: envStr("MINIO_ENDPOINT", ""),
|
||
MinIOAccessKey: envStr("MINIO_ACCESS_KEY", ""),
|
||
MinIOSecretKey: envStr("MINIO_SECRET_KEY", ""),
|
||
MinIOBucket: envStr("MINIO_BUCKET", "learning-videos"),
|
||
MinIOUseSSL: envBool("MINIO_USE_SSL", false),
|
||
RedisAddr: envStr("REDIS_ADDR", ""),
|
||
RedisPassword: envStr("REDIS_PASSWORD", ""),
|
||
RedisDB: envInt("REDIS_DB", 0),
|
||
PodName: envStr("POD_NAME", hostname()),
|
||
}
|
||
}
|
||
|
||
func envInt(key string, def int) int {
|
||
if v := os.Getenv(key); v != "" {
|
||
if n, err := strconv.Atoi(v); err == nil {
|
||
return n
|
||
}
|
||
}
|
||
return def
|
||
}
|
||
|
||
func envBool(key string, def bool) bool {
|
||
if v := os.Getenv(key); v != "" {
|
||
if b, err := strconv.ParseBool(v); err == nil {
|
||
return b
|
||
}
|
||
}
|
||
return def
|
||
}
|
||
|
||
func envStr(key, def string) string {
|
||
if v := os.Getenv(key); v != "" {
|
||
return v
|
||
}
|
||
return def
|
||
}
|
||
|
||
func hostname() string {
|
||
h, err := os.Hostname()
|
||
if err != nil {
|
||
return "unknown"
|
||
}
|
||
return h
|
||
}
|