package service import ( "context" "database/sql" "errors" "strings" "github.com/rogeecn/wxapp-kouqiang-guahao/backend/internal/db" ) const HomeHeroImageConfigKey = "home_hero_image" type AdminPhoneRecord struct { Phone string `json:"phone"` UserID string `json:"user_id"` OpenID string `json:"openid"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` LastLoginAt string `json:"last_login_at"` } type AdminBookingRecord struct { BookingID string `json:"booking_id"` Phone string `json:"phone"` ProjectID string `json:"project_id"` ProjectName string `json:"project_name"` Date string `json:"date"` StartTime string `json:"start_time"` Status string `json:"status"` StatusText string `json:"status_text"` CreatedAt string `json:"created_at"` } type AdminPriceInquiryRecord struct { InquiryID string `json:"inquiry_id"` Phone string `json:"phone"` Province string `json:"province"` City string `json:"city"` District string `json:"district"` Region string `json:"region"` ProjectName string `json:"project_name"` Status string `json:"status"` StatusClass string `json:"status_class"` StatusText string `json:"status_text"` Remark string `json:"remark"` LastSubmittedAt string `json:"last_submitted_at"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } type AdminPriceInquiryFilter struct { Area string Phone string Status string TimeFrom string TimeTo string } type AdminPriceInquiryPage struct { Records []AdminPriceInquiryRecord Total int64 Page int PageSize int TotalPages int HasPreviousPage bool HasNextPage bool PreviousPage int NextPage int } func (s *Service) HomeHeroImage(ctx context.Context) (string, error) { config, err := s.Q.GetAppConfig(ctx, HomeHeroImageConfigKey) if errors.Is(err, sql.ErrNoRows) { return "", nil } if err != nil { return "", err } return config.ConfigValue, nil } func (s *Service) SetHomeHeroImage(ctx context.Context, image string) error { _, err := s.Q.UpsertAppConfig(ctx, db.UpsertAppConfigParams{ ConfigKey: HomeHeroImageConfigKey, ConfigValue: image, }) return err } func (s *Service) AdminPhoneRecords(ctx context.Context) ([]AdminPhoneRecord, error) { users, err := s.Q.ListUsersWithPhone(ctx) if err != nil { return nil, err } records := make([]AdminPhoneRecord, 0, len(users)) for _, user := range users { phone := nullString(user.Phone) if phone == "" { continue } records = append(records, AdminPhoneRecord{ Phone: phone, UserID: user.ID, OpenID: user.Openid, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, LastLoginAt: user.LastLoginAt, }) } return records, nil } func (s *Service) AdminBookingRecords(ctx context.Context, phone string, limit, offset int64) ([]AdminBookingRecord, error) { phone = strings.TrimSpace(phone) var ( details []BookingDetail err error ) if phone != "" { details, err = s.ListBookingDetailsByPhone(ctx, phone) } else { details, err = s.AdminListBookingDetails(ctx, "", limit, offset) } if err != nil { return nil, err } records := make([]AdminBookingRecord, 0, len(details)) for _, detail := range details { records = append(records, AdminBookingRecord{ BookingID: detail.Booking.ID, Phone: detail.Booking.Phone, ProjectID: detail.Project.ID, ProjectName: detail.Project.Name, Date: detail.Day.Date, StartTime: detail.Slot.StartTime, Status: detail.Booking.Status, StatusText: adminBookingStatusText(detail.Booking.Status), CreatedAt: detail.Booking.CreatedAt, }) } return records, nil } func (s *Service) AdminPriceInquiryRecords(ctx context.Context, filter AdminPriceInquiryFilter, limit, offset int64) ([]AdminPriceInquiryRecord, error) { if limit <= 0 || limit > 200 { limit = 50 } filter = normalizeAdminPriceInquiryFilter(filter) inquiries, err := s.Q.ListPriceInquiries(ctx, db.ListPriceInquiriesParams{ Area: filter.Area, Phone: filter.Phone, Status: filter.Status, TimeFrom: filter.TimeFrom, TimeTo: filter.TimeTo, Limit: limit, Offset: offset, }) if err != nil { return nil, err } return adminPriceInquiryRecordsFromRows(inquiries), nil } func (s *Service) AdminPriceInquiryRecordsPage(ctx context.Context, filter AdminPriceInquiryFilter, page, pageSize int) (AdminPriceInquiryPage, error) { filter = normalizeAdminPriceInquiryFilter(filter) total, err := s.Q.CountPriceInquiries(ctx, db.CountPriceInquiriesParams{ Area: filter.Area, Phone: filter.Phone, Status: filter.Status, TimeFrom: filter.TimeFrom, TimeTo: filter.TimeTo, }) if err != nil { return AdminPriceInquiryPage{}, err } offset := int64((page - 1) * pageSize) inquiries, err := s.Q.ListPriceInquiries(ctx, db.ListPriceInquiriesParams{ Area: filter.Area, Phone: filter.Phone, Status: filter.Status, TimeFrom: filter.TimeFrom, TimeTo: filter.TimeTo, Limit: int64(pageSize), Offset: offset, }) if err != nil { return AdminPriceInquiryPage{}, err } totalPages := 1 if total > 0 { totalPages = int((total + int64(pageSize) - 1) / int64(pageSize)) } result := AdminPriceInquiryPage{ Records: adminPriceInquiryRecordsFromRows(inquiries), Total: total, Page: page, PageSize: pageSize, TotalPages: totalPages, HasPreviousPage: page > 1, HasNextPage: page < totalPages, } if result.HasPreviousPage { result.PreviousPage = page - 1 } if result.HasNextPage { result.NextPage = page + 1 } return result, nil } func adminPriceInquiryRecordsFromRows(inquiries []db.PriceInquiry) []AdminPriceInquiryRecord { records := make([]AdminPriceInquiryRecord, 0, len(inquiries)) for _, inquiry := range inquiries { records = append(records, AdminPriceInquiryRecord{ InquiryID: inquiry.ID, Phone: inquiry.Phone, Province: inquiry.Province, City: inquiry.City, District: inquiry.District, Region: inquiry.Province + " " + inquiry.City + " " + inquiry.District, ProjectName: inquiry.ProjectName, Status: inquiry.Status, StatusClass: adminPriceInquiryStatusClass(inquiry.Status), StatusText: adminPriceInquiryStatusText(inquiry.Status), Remark: inquiry.Remark, LastSubmittedAt: inquiry.LastSubmittedAt, CreatedAt: inquiry.CreatedAt, UpdatedAt: inquiry.UpdatedAt, }) } return records } func normalizeAdminPriceInquiryFilter(filter AdminPriceInquiryFilter) AdminPriceInquiryFilter { return AdminPriceInquiryFilter{ Area: strings.TrimSpace(filter.Area), Phone: strings.TrimSpace(filter.Phone), Status: strings.TrimSpace(filter.Status), TimeFrom: strings.TrimSpace(filter.TimeFrom), TimeTo: strings.TrimSpace(filter.TimeTo), } } func adminBookingStatusText(status string) string { switch status { case "pending": return "待确认" case "confirmed": return "已预约" case "cancelled_by_user": return "用户已取消" case "cancelled_by_admin": return "后台已取消" default: return status } } func adminPriceInquiryStatusText(status string) string { switch status { case "pending": return "待派单" case "assigned": return "已经派单" case "completed": return "已跟进" default: return status } } func adminPriceInquiryStatusClass(status string) string { switch status { case "pending": return "status-pending" case "assigned": return "status-assigned" case "completed": return "status-completed" default: return "status-unknown" } } func nullString(value sql.NullString) string { if !value.Valid { return "" } return value.String }