package api import ( "errors" "log/slog" "net/http" "strings" "unicode/utf8" "dana/internal/storage" ) // Старые адреса Bitrix, которых нет на новом сайте. Правила подобраны по картам сайта // и инфоблокам старой базы: удалённый врач ведёт в список специальности, позиция прайса — // в её раздел, лицензия — на страницу лицензий и т. д. Нечего предложить — честный 404. // legacyTarget возвращает адрес, куда перенаправить старую ссылку, или "". func (a *API) legacyTarget(r *http.Request, parts []string) string { ctx := r.Context() if len(parts) == 0 { return "" } sectionPage := map[string]string{ "licenses": "/company/licenses/", "docs": "/legaldocs/", "nadzor": "/company/nadzor/", "partners": "/company/partners/", "vacancy": "/company/vacancy/", "reviews": "/reviews/", "history": "/company/", "requisites": "/legaldocs/", } switch parts[0] { case "company": if len(parts) >= 2 && parts[1] == "staff" { return a.legacyDoctor(r, parts) } if len(parts) >= 2 && sectionPage[parts[1]] != "" { return sectionPage[parts[1]] } return "/company/" case "price": return a.legacyPrice(r, parts) case "services": if len(parts) >= 2 { if _, err := a.store.ServiceByPath(ctx, parts[1]); err == nil { return "/services/" + parts[1] + "/" } } return "/services/" case "checkup": return "/checkup/" case "news", "articles": return "/" + parts[0] + "/" case "contacts": return "/contacts/" case "info": if len(parts) == 1 || parts[1] == "faq" { return "/info/faq/" } case "patients": // страница «Пациенту» перенесена скрытой; пока клиника её не включила — ближайший раздел return "/info/faq/" } return "" } // legacyDoctor: /company/staff/<раздел>/<врач>/. Врач мог сменить специальность — тогда // ведём на его текущий адрес; адреса со старыми хвостами («-det», «_uro», «-») — к тому же врачу. func (a *API) legacyDoctor(r *http.Request, parts []string) string { ctx := r.Context() if len(parts) >= 4 { slug := parts[3] candidates := []string{slug, strings.TrimRight(slug, "-"), slug + "-"} for _, suffix := range []string{"-det", "_uro", "_det"} { if strings.HasSuffix(slug, suffix) { candidates = append(candidates, strings.TrimSuffix(slug, suffix)) } } for _, c := range candidates { if d, err := a.store.DoctorBySlug(ctx, c); err == nil { return doctorWebPath(*d) } } } if len(parts) >= 3 { if ok, _ := a.knownContentRoute(r, parts[:3]); ok { return "/company/staff/" + parts[2] + "/" } } return "/company/staff/" } // legacyPrice: страницы отдельных позиций прайса и старые вложенные разделы // (/price/analizy/allergologiya/…) — в ближайший раздел, который есть на новом сайте. func (a *API) legacyPrice(r *http.Request, parts []string) string { ctx := r.Context() for i := len(parts) - 1; i >= 1; i-- { p := parts[i] if strings.HasPrefix(p, "chek-ap-") { if _, err := a.store.CheckupBySlug(ctx, p); err == nil { return "/checkup/" + p + "/" } } if ok, _ := a.knownContentRoute(r, []string{"price", p}); ok { return "/price/" + p + "/" } } return "/price/" } // notFound отвечает на адрес, которого нет: ручной редирект из админки, правило для // старых адресов или 404 с записью в журнал. func (a *API) notFound(w http.ResponseWriter, r *http.Request, pagePath string, parts []string) { if a.store == nil { // юнит-тесты сайта без базы a.website404(w, r) return } // файлы (/index.php) хранятся без слэша на конце, разделы — со слэшем for _, p := range []string{r.URL.Path, pagePath} { if to, code, err := a.store.RedirectFor(r.Context(), p); err == nil { redirectTo(w, r, to, code) return } else if !errors.Is(err, storage.ErrNotFound) { slog.Error("redirect lookup", "err", err) } if p == pagePath { break } } if to := a.legacyTarget(r, parts); to != "" && to != pagePath { redirectTo(w, r, to, http.StatusMovedPermanently) return } logged := pagePath if last := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:]; strings.Contains(last, ".") { logged = r.URL.Path // файл, а не раздел: без добавленного слэша } if len(logged) <= 300 && utf8.ValidString(logged) { if err := a.store.LogNotFound(r.Context(), logged, truncate(r.Referer(), 300)); err != nil { slog.Error("log not found", "err", err) } } a.website404(w, r) } func redirectTo(w http.ResponseWriter, r *http.Request, to string, code int) { if code != http.StatusFound { code = http.StatusMovedPermanently } if r.URL.RawQuery != "" && !strings.Contains(to, "?") { to += "?" + r.URL.RawQuery } http.Redirect(w, r, to, code) } func truncate(s string, n int) string { if len(s) <= n { return s } for n > 0 && !utf8.RuneStart(s[n]) { n-- } return s[:n] }