package mall import ( "context" "html" "net/url" "regexp" "strings" "unicode/utf8" "git.118im.com/im_team/im_workbench/internal/store" ) const ( defaultPageSize = 20 maxPageSize = 100 maxSummaryRunes = 120 ) var ( scriptBlockPattern = regexp.MustCompile(`(?is)]*>.*?`) styleBlockPattern = regexp.MustCompile(`(?is)]*>.*?`) htmlTagPattern = regexp.MustCompile(`(?s)<[^>]*>`) ) type Repository interface { ManualProducts(context.Context, int64, int64, int, int) (*store.ManualProductPage, error) ManualProduct(context.Context, int64, int64) (*store.ManualProduct, error) } type Service struct { repository Repository } type Product struct { ID int64 `json:"id"` ProductNo string `json:"product_no"` Category Category `json:"category"` Name string `json:"name"` ShopName string `json:"shop_name"` PriceAmount int64 `json:"price_amount"` Currency string `json:"currency"` CoverURL string `json:"cover_url"` Summary string `json:"summary"` ClaimedCount int64 `json:"claimed_count"` IsRecommended bool `json:"is_recommended"` IsLimited bool `json:"is_limited"` StartAt string `json:"start_at"` EndAt string `json:"end_at"` AvailableActions []string `json:"available_actions"` PaymentType string `json:"payment_type,omitempty"` DetailText string `json:"detail_text,omitempty"` CreatedAt string `json:"created_at,omitempty"` UpdatedAt string `json:"updated_at,omitempty"` } type Category struct { ID int64 `json:"id"` Code string `json:"code"` Name string `json:"name"` } type ProductPage struct { Items []Product `json:"items"` Page int `json:"page"` PageSize int `json:"page_size"` Total int64 `json:"total"` } func New(repository Repository) *Service { return &Service{repository: repository} } func (s *Service) Products(ctx context.Context, tenantID, categoryID int64, page, pageSize int) (*ProductPage, error) { if categoryID < 0 { return nil, ErrCategoryIDInvalid } if page == 0 { page = 1 } if pageSize == 0 { pageSize = defaultPageSize } if page < 1 { return nil, ErrPageInvalid } if pageSize < 1 || pageSize > maxPageSize { return nil, ErrPageSizeInvalid } rows, err := s.repository.ManualProducts(ctx, tenantID, categoryID, (page-1)*pageSize, pageSize) if err != nil { return nil, err } items := make([]Product, 0, len(rows.Items)) for _, row := range rows.Items { items = append(items, productFromStore(row, false)) } return &ProductPage{Items: items, Page: page, PageSize: pageSize, Total: rows.Total}, nil } func (s *Service) Product(ctx context.Context, tenantID, productID int64) (*Product, error) { if productID <= 0 { return nil, ErrProductIDInvalid } row, err := s.repository.ManualProduct(ctx, tenantID, productID) if err != nil { return nil, err } result := productFromStore(*row, true) return &result, nil } func productFromStore(row store.ManualProduct, detail bool) Product { detailText := plainText(row.DetailHTML) result := Product{ID: row.ID, ProductNo: row.ProductNo, Category: Category{ID: row.CategoryID, Code: row.CategoryCode, Name: row.CategoryName}, Name: row.Name, ShopName: row.ShopName, PriceAmount: row.PriceAmount, Currency: row.Currency, CoverURL: safeHTTPSURL(row.CoverURL), Summary: truncate(detailText, maxSummaryRunes), ClaimedCount: row.ClaimedCount, IsRecommended: row.IsRecommended, IsLimited: row.EndAt != "", StartAt: row.StartAt, EndAt: row.EndAt, AvailableActions: []string{"contact_support"}} if detail { result.PaymentType = row.PaymentType result.DetailText = detailText result.CreatedAt = row.CreatedAt result.UpdatedAt = row.UpdatedAt } return result } func plainText(raw string) string { raw = scriptBlockPattern.ReplaceAllString(raw, " ") raw = styleBlockPattern.ReplaceAllString(raw, " ") withoutTags := htmlTagPattern.ReplaceAllString(raw, " ") return strings.Join(strings.Fields(html.UnescapeString(withoutTags)), " ") } func truncate(value string, limit int) string { if utf8.RuneCountInString(value) <= limit { return value } runes := []rune(value) return string(runes[:limit]) + "…" } func safeHTTPSURL(raw string) string { raw = strings.TrimSpace(raw) parsed, err := url.Parse(raw) if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil { return "" } return parsed.String() }