package web import ( "fmt" "net/http" "strconv" "strings" "daili/internal/domain" "daili/internal/store" ) type packagesData struct { List []store.Package } func (s *Server) handlePackages(w http.ResponseWriter, r *http.Request) { list, err := s.db.ListPackages(true) if err != nil { http.Error(w, "读取套餐失败", http.StatusInternalServerError) return } p := s.newPage(r, "套餐", "packages") p.D = &packagesData{List: list} s.render(w, r, "packages", p) } type packageFormData struct { Package *store.Package IsNew bool Tags []string } func (s *Server) handlePackageNewPage(w http.ResponseWriter, r *http.Request) { p := s.newPage(r, "新增套餐", "packages") tags, _ := s.db.PackageTags() p.D = &packageFormData{ Package: &store.Package{Unit: domain.UnitDay, DefaultAmount: 30, Color: "#2F5D50"}, IsNew: true, Tags: tags, } s.render(w, r, "package_form", p) } func readPackageForm(r *http.Request) *store.Package { p := &store.Package{ Name: strings.TrimSpace(r.PostFormValue("name")), Unit: strings.TrimSpace(r.PostFormValue("unit")), DefaultAmount: 0, Color: strings.TrimSpace(r.PostFormValue("color")), Note: strings.TrimSpace(r.PostFormValue("note")), SortOrder: 0, } if v, err := strconv.Atoi(strings.TrimSpace(r.PostFormValue("default_amount"))); err == nil { p.DefaultAmount = v } if v, err := strconv.Atoi(strings.TrimSpace(r.PostFormValue("sort_order"))); err == nil { p.SortOrder = v } if price := strings.TrimSpace(r.PostFormValue("price")); price != "" { if f, err := strconv.ParseFloat(price, 64); err == nil { p.PriceCents = int(f*100 + 0.5) p.HasPrice = true } } if p.Unit == "" { p.Unit = domain.UnitDay } p.CalendarSeparate = r.PostFormValue("calendar_separate") != "" p.Tag = strings.TrimSpace(r.PostFormValue("tag")) if p.Unit == "season" { p.Season = true p.Unit = domain.UnitDay } if v, err := strconv.Atoi(strings.TrimSpace(r.PostFormValue("default_validity_days"))); err == nil { p.DefaultValidityDays = v } p.SeasonStart = strings.TrimSpace(r.PostFormValue("season_start")) p.SeasonEnd = strings.TrimSpace(r.PostFormValue("season_end")) return p } func validatePackage(p *store.Package) string { if p.Season { if p.SeasonStart == "" || p.SeasonEnd == "" { return "包赛季套餐必须填写赛季开始日和结束日" } if p.SeasonEnd < p.SeasonStart { return "赛季结束日不能早于开始日" } } if p.Name == "" { return "套餐名称不能为空" } if len([]rune(p.Name)) > 32 { return "套餐名称最长 32 字" } if p.Unit != domain.UnitDay && p.Unit != domain.UnitCount { return "套餐类型不正确" } if p.DefaultAmount <= 0 { return "默认天数/次数必须大于 0" } if p.Color == "" || !strings.HasPrefix(p.Color, "#") { return "请选择套餐颜色" } return "" } // packageDetailData 套餐详情页数据。 type packageDetailData struct { Package *store.Package Customers []store.Customer Subscribed map[int64]bool Q string Tags []string Today string } // handlePackageDetail 套餐详情:查看信息 + 勾选客户批量开通。 func (s *Server) handlePackageDetail(w http.ResponseWriter, r *http.Request) { id := pathID(r) pkg, err := s.db.Package(id) if err != nil { http.NotFound(w, r) return } today := domain.Today() warn := s.warnDays() q := strings.TrimSpace(r.URL.Query().Get("q")) list, _, err := s.db.ListCustomers(q, 300, 0, domain.Date(today), warn, false) if err != nil { http.Error(w, "读取客户失败", http.StatusInternalServerError) return } subscribed, err := s.db.SubscribedCustomerIDs(id) if err != nil { http.Error(w, "读取订阅失败", http.StatusInternalServerError) return } tags, _ := s.db.PackageTags() p := s.newPage(r, "套餐详情", "packages") p.D = &packageDetailData{Package: pkg, Customers: list, Subscribed: subscribed, Q: q, Tags: tags, Today: domain.Date(today)} s.render(w, r, "package_detail", p) } // handlePackageBatchSubscribe 给勾选的客户批量开通该套餐。 func (s *Server) handlePackageBatchSubscribe(w http.ResponseWriter, r *http.Request) { id := pathID(r) pkg, err := s.db.Package(id) if err != nil { http.NotFound(w, r) return } _ = r.ParseForm() back := "/packages/" + itoa(id) raw := r.PostForm["customer_ids"] if len(raw) == 0 { s.redirect(w, r, back, "没有选中任何客户") return } start := strings.TrimSpace(r.PostFormValue("start_date")) if start == "" { start = domain.Date(domain.Today()) } if _, err := domain.ParseDate(start); err != nil { s.redirect(w, r, back, "开始日期格式不正确") return } end := strings.TrimSpace(r.PostFormValue("end_date")) days := s.formInt(r, "days", 0) count := s.formInt(r, "total_count", 0) u := s.currentUser(r) var op int64 if u != nil { op = u.ID } done, skipped, failed := 0, 0, 0 var lastErr string for _, v := range raw { cid, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) if err != nil { failed++ continue } _, err = s.db.CreateSub(store.CreateSubInput{ CustomerID: cid, PackageID: id, StartDate: start, EndDate: end, Days: days, TotalCount: count, OperatorID: op, }) if err != nil { if strings.Contains(err.Error(), "已订阅") { skipped++ continue } failed++ lastErr = err.Error() continue } done++ } s.logOp(r, "批量开通", "package", id, fmt.Sprintf("%s:成功 %d、跳过 %d、失败 %d", pkg.Name, done, skipped, failed)) msg := fmt.Sprintf("已为 %d 位客户开通「%s」", done, pkg.Name) if skipped > 0 { msg += fmt.Sprintf(",%d 位已有该套餐已跳过", skipped) } if failed > 0 { msg += fmt.Sprintf(",%d 位失败(%s)", failed, lastErr) } s.redirect(w, r, back, msg) } func (s *Server) handlePackageCreate(w http.ResponseWriter, r *http.Request) { pkg := readPackageForm(r) p := s.newPage(r, "新增套餐", "packages") if msg := validatePackage(pkg); msg != "" { tags, _ := s.db.PackageTags() p.D = &packageFormData{Package: pkg, IsNew: true, Tags: tags} s.fail(w, r, "package_form", msg, p) return } id, err := s.db.CreatePackage(pkg) if err != nil { tags, _ := s.db.PackageTags() p.D = &packageFormData{Package: pkg, IsNew: true, Tags: tags} s.fail(w, r, "package_form", "保存失败:"+err.Error(), p) return } s.logOp(r, "新增套餐", "package", id, "新增套餐 "+pkg.Name) s.redirect(w, r, "/packages", "套餐已创建,现在可以给客户开通了") } func (s *Server) handlePackageEditPage(w http.ResponseWriter, r *http.Request) { id := pathID(r) pkg, err := s.db.Package(id) if err != nil { http.NotFound(w, r) return } p := s.newPage(r, "编辑套餐", "packages") tags, _ := s.db.PackageTags() p.D = &packageFormData{Package: pkg, IsNew: false, Tags: tags} s.render(w, r, "package_form", p) } func (s *Server) handlePackageUpdate(w http.ResponseWriter, r *http.Request) { id := pathID(r) pkg := readPackageForm(r) pkg.ID = id p := s.newPage(r, "编辑套餐", "packages") if msg := validatePackage(pkg); msg != "" { tags, _ := s.db.PackageTags() p.D = &packageFormData{Package: pkg, IsNew: false, Tags: tags} s.fail(w, r, "package_form", msg, p) return } if err := s.db.UpdatePackage(pkg); err != nil { tags, _ := s.db.PackageTags() p.D = &packageFormData{Package: pkg, IsNew: false, Tags: tags} s.fail(w, r, "package_form", "保存失败:"+err.Error(), p) return } s.logOp(r, "编辑套餐", "package", id, "修改套餐 "+pkg.Name) s.redirect(w, r, "/packages", "套餐已保存") } func (s *Server) handlePackageArchive(w http.ResponseWriter, r *http.Request) { id := pathID(r) pkg, err := s.db.Package(id) if err != nil { http.NotFound(w, r) return } archived := !pkg.Archived if err := s.db.SetPackageArchived(id, archived); err != nil { s.redirect(w, r, "/packages", "操作失败:"+err.Error()) return } action := "归档套餐" msg := "套餐已归档,不再允许新订阅(已有订阅不受影响)" if !archived { action = "取消归档" msg = "套餐已恢复" } s.logOp(r, action, "package", id, action+" "+pkg.Name) s.redirect(w, r, "/packages", msg) }