dana-medical-site/internal/api/legacy.go
2026-10-14 13:33:13 +00:00

161 lines
5.3 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 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]
}