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)`)
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()
}