- 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
196 lines
6.5 KiB
Go
196 lines
6.5 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|