dana-medical-site/internal/config/config.go
Abdulkadyr Karagishiev 121f286016 feat(crm): integrate website leads and online booking with new CRM and 1C
- Add internal/crm client with CanonicalJSON and HMAC-SHA256 signing for POST /api/public/leads
- Add database migration 0026_crm_deals.sql with crm_deal_id and partial indexes
- Add async lead intake dispatch to new CRM in handlers_leads.go
- Support dual-mode booking confirmation (CRM_MODE=both/crm_only/bitrix_only) in booking_confirm.go
- Add LinkCRM endpoint to bridge_1c.py and onec.Client
- Update config and env examples with CRM_API_URL, CRM_LEAD_INTAKE_SECRET, CRM_MODE
2026-10-16 18:01:49 +03:00

196 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package config
import (
"fmt"
"os"
"strconv"
"strings"
"time"
)
type Config struct {
Addr string
DatabaseURL string
MediaDir string
AllowedOrigins string
AdminEmail string
AdminPassword string
ShutdownTimeout time.Duration
Debug bool
SiteURL string
WebDir string
SSRRenderer string
NodeBinary string
// Уведомления о заявках на почту. Без SMTP_HOST отключены.
SMTPHost string
SMTPPort int
SMTPUser string
SMTPPassword string
NotifyFrom string
NotifyTo []string
// Виртуальная АТС МегаФон: адрес ВАТС (vats970345.megapbx.ru) и ключ REST API.
// Без ключа загрузка звонков выключена.
MegafonURL string
MegafonKey string
// АТС МегаФон клиники «Очевидно» (vats363024.megapbx.ru) — свои звонки и свой отчёт по каналам.
OchevidnoMegafonURL string
OchevidnoMegafonKey string
// 1С: расписание врачей и запись. Без адреса и ключа запись через 1С выключена, остаются заявки.
OnecURL string
OnecKey string
OnecAuthHeader string
OnecWebhookKey string
OnecSyncSecs int // период опроса 1С, по умолчанию 300
OnecLongPoll bool // режим long polling: 1С держит запрос /changes
// Битрикс24: входящий вебхук для разовой выгрузки номеров лидов/контактов в «Отчёт по каналам».
BitrixWebhookURL string
// Новая CRM (dana-medccrm): шлюз создания сделок и режим работы.
CrmAPIURL string
CrmLeadIntakeSecret string
CrmMode string // both (по умолчанию) | crm_only | bitrix_only
// SMSC.RU: подтверждение номера звонком (Wait-Call)
SMSCLogin string
SMSCPassword string
// Секрет в адресе обратного вызова SMSC (?token=…). Без него вызов не принимается.
SMSCCallbackToken string
// TrustedProxies — адреса/подсети прокси, чьим X-Real-IP верим (пусто — только сам сервер).
TrustedProxies string
// Кабинеты ПроДокторов и СберЗдоровья (скачивание с сервера; пусто = выключено).
ProdoctorovCookie string
ProdoctorovLPU string
SberHealthCookie string
SberHealthClinic string
}
func Load() (Config, error) {
loadDotEnv(".env")
c := Config{
Addr: env("APP_ADDR", ":8080"),
DatabaseURL: os.Getenv("DATABASE_URL"),
MediaDir: env("MEDIA_DIR", "./media"),
AllowedOrigins: env("ALLOWED_ORIGINS", "*"),
AdminEmail: env("ADMIN_EMAIL", ""),
AdminPassword: env("ADMIN_PASSWORD", ""),
ShutdownTimeout: 15 * time.Second,
Debug: envBool("DEBUG", false),
SiteURL: env("SITE_URL", ""),
WebDir: env("WEB_DIR", "./web/dist"),
SSRRenderer: env("SSR_RENDERER", "./web/scripts/render-page.mjs"),
NodeBinary: env("NODE_BINARY", "node"),
SMTPHost: env("SMTP_HOST", ""),
SMTPPort: envInt("SMTP_PORT", 465),
SMTPUser: env("SMTP_USER", ""),
SMTPPassword: os.Getenv("SMTP_PASSWORD"),
NotifyFrom: env("NOTIFY_FROM", os.Getenv("SMTP_USER")),
NotifyTo: envList("NOTIFY_TO"),
MegafonURL: env("MEGAFON_API_URL", ""),
MegafonKey: os.Getenv("MEGAFON_API_KEY"),
OchevidnoMegafonURL: env("OCHEVIDNO_MEGAFON_API_URL", ""),
OchevidnoMegafonKey: os.Getenv("OCHEVIDNO_MEGAFON_API_KEY"),
OnecURL: env("ONEC_API_URL", ""),
OnecKey: os.Getenv("ONEC_API_KEY"),
OnecAuthHeader: env("ONEC_AUTH_HEADER", "X-Api-Key"),
OnecWebhookKey: os.Getenv("ONEC_WEBHOOK_KEY"),
OnecSyncSecs: envInt("ONEC_SYNC_SECONDS", 300),
OnecLongPoll: os.Getenv("ONEC_LONGPOLL") == "1",
BitrixWebhookURL: os.Getenv("BITRIX_WEBHOOK_URL"),
CrmAPIURL: env("CRM_API_URL", "https://stagingcrm.dana-medical.ru"),
CrmLeadIntakeSecret: os.Getenv("CRM_LEAD_INTAKE_SECRET"),
CrmMode: env("CRM_MODE", "both"),
SMSCLogin: os.Getenv("SMSC_LOGIN"),
SMSCPassword: os.Getenv("SMSC_PASSWORD"),
SMSCCallbackToken: os.Getenv("SMSC_CALLBACK_TOKEN"),
TrustedProxies: os.Getenv("TRUSTED_PROXIES"),
ProdoctorovCookie: os.Getenv("PRODOCTOROV_COOKIE"),
ProdoctorovLPU: os.Getenv("PRODOCTOROV_LPU_ID"),
SberHealthCookie: os.Getenv("SBERHEALTH_COOKIE"),
SberHealthClinic: os.Getenv("SBERHEALTH_CLINIC_ID"),
}
if c.DatabaseURL == "" {
return c, fmt.Errorf("DATABASE_URL is required")
}
if c.SMTPHost != "" && c.NotifyFrom == "" {
return c, fmt.Errorf("NOTIFY_FROM or SMTP_USER is required when SMTP_HOST is set")
}
return c, nil
}
func envInt(key string, def int) int {
n, err := strconv.Atoi(os.Getenv(key))
if err != nil {
return def
}
return n
}
// envList читает список через запятую: «a@x.ru, b@x.ru».
func envList(key string) []string {
var out []string
for _, part := range strings.Split(os.Getenv(key), ",") {
if part = strings.TrimSpace(part); part != "" {
out = append(out, part)
}
}
return out
}
func env(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func envBool(key string, def bool) bool {
v := os.Getenv(key)
if v == "" {
return def
}
b, err := strconv.ParseBool(v)
if err != nil {
return def
}
return b
}
// loadDotEnv читает файл .env рядом с сервером: строки «КЛЮЧ=значение», # — комментарий.
// Переменные, уже заданные в окружении, не перезаписываются. Файл в git не попадает (.gitignore):
// в нём живут секреты — пароль базы, ключ API АТС.
func loadDotEnv(path string) {
data, err := os.ReadFile(path)
if err != nil {
return
}
text := strings.TrimPrefix(string(data), "\ufeff")
for _, line := range strings.Split(text, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
k = strings.TrimSpace(strings.TrimPrefix(k, "export "))
v = strings.TrimSpace(v)
if len(v) >= 2 && ((v[0] == '"' && v[len(v)-1] == '"') || (v[0] == '\'' && v[len(v)-1] == '\'')) {
v = v[1 : len(v)-1]
}
if _, set := os.LookupEnv(k); !set && k != "" {
_ = os.Setenv(k, v)
}
}
}