package web import ( "fmt" "net/http" "strings" "time" "daili/internal/domain" "daili/internal/store" ) // portalDay 自助查询月历中的一天。 type portalDay struct { Date string Day int InMonth bool Today bool Total int Done int } // portalSub 自助查询里展示的订阅(隐藏备注、打手、价格)。 type portalSub struct { Name string UnitLabel string Status string Period string TodayDone bool } // portalAsk 同一尾号命中多个客户时要求补姓名。 type portalAsk struct { Count int Names []string } // portalView 查询结果。 type portalView struct { Tail string Name string Game string Subs []portalSub Weeks [][]portalDay MonthTitle string Prev string Next string TodayTotal int TodayDone int AllDone bool ActiveSubs int Expiring string TodayPct int Count int Names []string } // handleCustomerPortal 客户自助查询页(公开,无入口链接)。 func (s *Server) handleCustomerPortal(w http.ResponseWriter, r *http.Request) { p := s.newPage(r, "排单查询", "") q := r.URL.Query() tail := strings.TrimSpace(q.Get("tail")) name := strings.TrimSpace(q.Get("name")) month := strings.TrimSpace(q.Get("cm")) p.Query = tail if tail == "" { s.render(w, r, "portal", p) return } list, err := s.db.CustomersByTail(tail) if err != nil { p.Error = "读取客户失败,请稍后再试" s.render(w, r, "portal", p) return } list = filterActive(list) if len(list) == 0 { p.Error = "没有找到这个尾号的客户,请核对尾号" s.render(w, r, "portal", p) return } if len(list) > 1 { if name == "" { names := make([]string, 0, len(list)) for _, c := range list { names = append(names, c.Name) } p.D = &portalAsk{Count: len(list), Names: names} s.render(w, r, "portal", p) return } matched := matchByName(list, name) if len(matched) == 0 { p.Error = "尾号对得上,但姓名不匹配,请再确认一下" s.render(w, r, "portal", p) return } if len(matched) > 1 { names := make([]string, 0, len(matched)) for _, c := range matched { names = append(names, c.Name) } p.D = &portalAsk{Count: len(matched), Names: names} s.render(w, r, "portal", p) return } list = matched } c := list[0] p.D = s.buildPortal(c, tail, name, month) s.render(w, r, "portal", p) } func filterActive(list []store.Customer) []store.Customer { out := list[:0] for _, c := range list { if !c.Archived { out = append(out, c) } } return out } func matchByName(list []store.Customer, name string) []store.Customer { needle := strings.ToLower(strings.TrimSpace(name)) out := make([]store.Customer, 0, len(list)) for _, c := range list { if strings.Contains(strings.ToLower(c.Name), needle) { out = append(out, c) } } return out } // buildPortal 组装某位客户的排单视图。 func (s *Server) buildPortal(c store.Customer, tail, name, month string) *portalView { today := domain.Today() todayStr := domain.Date(today) first := time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, time.UTC) if month != "" { if t, err := time.Parse("2006-01", month); err == nil { first = t } } year, mon := first.Year(), int(first.Month()) firstDay := time.Date(year, time.Month(mon), 1, 0, 0, 0, 0, time.UTC) lastDay := firstDay.AddDate(0, 1, -1) start := firstDay.AddDate(0, 0, -((int(firstDay.Weekday()) + 6) % 7)) end := lastDay.AddDate(0, 0, 6-((int(lastDay.Weekday())+6)%7)) subs, _ := s.db.SubsByCustomer(c.ID) active := make([]store.Subscription, 0, len(subs)) for _, sub := range subs { if sub.Status != domain.StatusClosed { active = append(active, sub) } } cov := func(sub store.Subscription, date string) bool { if date < sub.StartDate { return false } if sub.EndDate == "" { return true } return date <= sub.EndDate } // 打卡记录 cks, _ := s.db.CheckinsBetween(domain.Date(start), domain.Date(end)) v := &portalView{Tail: c.Tail, Name: c.Name, Game: c.GameName, ActiveSubs: len(active)} v.MonthTitle = fmt.Sprintf("%d 年 %d 月", year, mon) q := "?tail=" + tail if name != "" { q += "&name=" + name } v.Prev = q + "&cm=" + firstDay.AddDate(0, -1, 0).Format("2006-01") v.Next = q + "&cm=" + firstDay.AddDate(0, 1, 0).Format("2006-01") for _, sub := range active { label := "按日" if sub.Season { label = "包赛季" } else if sub.Unit == domain.UnitCount { label = "按次" } period := sub.StartDate if sub.EndDate != "" { period += " → " + sub.EndDate } else { period += " 起 · 无有效期" } ps := portalSub{Name: sub.PackageName, UnitLabel: label, Status: sub.Status, Period: period} if m, ok := cks[sub.ID]; ok { if ck, ok2 := m[todayStr]; ok2 && ck.Status == "done" { ps.TodayDone = true } } v.Subs = append(v.Subs, ps) if sub.EndDate != "" && (v.Expiring == "" || sub.EndDate < v.Expiring) { v.Expiring = sub.EndDate } } // 月历 days := int(end.Sub(start).Hours()/24) + 1 week := make([]portalDay, 0, 7) for i := 0; i < days; i++ { d := start.AddDate(0, 0, i) ds := domain.Date(d) pd := portalDay{Date: ds, Day: d.Day(), InMonth: d.Month() == time.Month(mon), Today: ds == todayStr} for _, sub := range active { if !cov(sub, ds) { continue } pd.Total++ if m, ok := cks[sub.ID]; ok { if ck, ok2 := m[ds]; ok2 && ck.Status == "done" { pd.Done++ } } } if pd.Today { v.TodayTotal, v.TodayDone = pd.Total, pd.Done } week = append(week, pd) if len(week) == 7 { v.Weeks = append(v.Weeks, week) week = make([]portalDay, 0, 7) } } if len(week) > 0 { v.Weeks = append(v.Weeks, week) } v.AllDone = v.TodayTotal > 0 && v.TodayDone >= v.TodayTotal if v.TodayTotal > 0 { v.TodayPct = v.TodayDone * 100 / v.TodayTotal } return v }