feat: migrate workbench services
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
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)<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"`
|
||||
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,
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user