154 lines
4.5 KiB
Go
154 lines
4.5 KiB
Go
package mall
|
|
|
|
import (
|
|
"context"
|
|
"html"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"git.118im.com/im_team/olons_server/internal/store"
|
|
)
|
|
|
|
const (
|
|
defaultPageSize = 20
|
|
maxPageSize = 100
|
|
maxSummaryRunes = 120
|
|
)
|
|
|
|
var (
|
|
scriptBlockPattern = regexp.MustCompile(`(?is)<script\b[^>]*>.*?</script\s*>`)
|
|
styleBlockPattern = regexp.MustCompile(`(?is)<style\b[^>]*>.*?</style\s*>`)
|
|
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()
|
|
}
|