149 lines
3.7 KiB
Go
149 lines
3.7 KiB
Go
|
|
package knowledge
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"encoding/json"
|
|||
|
|
"strings"
|
|||
|
|
|
|||
|
|
branddomain "haixun-backend/internal/model/brand/domain/usecase"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// PatrolTagInput grounds海巡 tag in研究地圖與品牌/產品輸入。
|
|||
|
|
type PatrolTagInput struct {
|
|||
|
|
BrandName string
|
|||
|
|
ProductName string
|
|||
|
|
ProductFeatures string
|
|||
|
|
AudienceSummary string
|
|||
|
|
MatchTags []string
|
|||
|
|
Questions []string
|
|||
|
|
Pillars []string
|
|||
|
|
PatrolKeywords []string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type productContextFields struct {
|
|||
|
|
Brand string `json:"brand"`
|
|||
|
|
Product string `json:"product"`
|
|||
|
|
Features string `json:"features"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func parseProductContextFields(raw string) productContextFields {
|
|||
|
|
raw = strings.TrimSpace(raw)
|
|||
|
|
if raw == "" {
|
|||
|
|
return productContextFields{}
|
|||
|
|
}
|
|||
|
|
var parsed productContextFields
|
|||
|
|
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
|
|||
|
|
return productContextFields{Features: raw}
|
|||
|
|
}
|
|||
|
|
parsed.Brand = strings.TrimSpace(parsed.Brand)
|
|||
|
|
parsed.Product = strings.TrimSpace(parsed.Product)
|
|||
|
|
parsed.Features = strings.TrimSpace(parsed.Features)
|
|||
|
|
return parsed
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func PatrolTagInputFromBrand(brand *branddomain.BrandSummary, productBrief string) PatrolTagInput {
|
|||
|
|
if brand == nil {
|
|||
|
|
return PatrolTagInput{}
|
|||
|
|
}
|
|||
|
|
fields := parseProductContextFields(brand.ProductContext)
|
|||
|
|
productName := productLabelFromBrand(brand)
|
|||
|
|
if productName == "" {
|
|||
|
|
productName = fields.Product
|
|||
|
|
}
|
|||
|
|
brandName := strings.TrimSpace(brand.DisplayName)
|
|||
|
|
if brandName == "" {
|
|||
|
|
brandName = fields.Brand
|
|||
|
|
}
|
|||
|
|
features := fields.Features
|
|||
|
|
if features == "" {
|
|||
|
|
features = strings.TrimSpace(productBrief)
|
|||
|
|
}
|
|||
|
|
if features == "" {
|
|||
|
|
features = strings.TrimSpace(brand.ProductBrief)
|
|||
|
|
}
|
|||
|
|
return PatrolTagInput{
|
|||
|
|
BrandName: brandName,
|
|||
|
|
ProductName: productName,
|
|||
|
|
ProductFeatures: features,
|
|||
|
|
AudienceSummary: strings.TrimSpace(brand.ResearchMap.AudienceSummary),
|
|||
|
|
MatchTags: matchTagsFromBrand(brand),
|
|||
|
|
Questions: append([]string{}, brand.ResearchMap.Questions...),
|
|||
|
|
Pillars: append([]string{}, brand.ResearchMap.Pillars...),
|
|||
|
|
PatrolKeywords: append([]string{}, brand.ResearchMap.PatrolKeywords...),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func productLabelFromBrand(brand *branddomain.BrandSummary) string {
|
|||
|
|
if brand == nil {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
productID := strings.TrimSpace(brand.ProductID)
|
|||
|
|
if productID == "" {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
for _, item := range brand.Products {
|
|||
|
|
if item.ID == productID {
|
|||
|
|
label := strings.TrimSpace(item.Label)
|
|||
|
|
if label != "" {
|
|||
|
|
return label
|
|||
|
|
}
|
|||
|
|
return parseProductContextFields(item.ProductContext).Product
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func matchTagsFromBrand(brand *branddomain.BrandSummary) []string {
|
|||
|
|
if brand == nil {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
productID := strings.TrimSpace(brand.ProductID)
|
|||
|
|
seen := map[string]struct{}{}
|
|||
|
|
out := []string{}
|
|||
|
|
add := func(tag string) {
|
|||
|
|
tag = strings.TrimSpace(tag)
|
|||
|
|
if tag == "" {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if _, ok := seen[tag]; ok {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
seen[tag] = struct{}{}
|
|||
|
|
out = append(out, tag)
|
|||
|
|
}
|
|||
|
|
for _, item := range brand.Products {
|
|||
|
|
if productID != "" && item.ID != productID {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
for _, tag := range item.MatchTags {
|
|||
|
|
add(tag)
|
|||
|
|
}
|
|||
|
|
if productID != "" {
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return out
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (in PatrolTagInput) HasResearchMap() bool {
|
|||
|
|
return len(in.Questions) > 0 || len(in.Pillars) > 0 || strings.TrimSpace(in.AudienceSummary) != ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (in PatrolTagInput) HasProductContext() bool {
|
|||
|
|
return strings.TrimSpace(in.ProductName) != "" ||
|
|||
|
|
strings.TrimSpace(in.ProductFeatures) != "" ||
|
|||
|
|
len(in.MatchTags) > 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func productCategoryHint(productName, features string) string {
|
|||
|
|
combined := productName + " " + features
|
|||
|
|
for _, hint := range []string{
|
|||
|
|
"沐浴乳", "沐浴露", "洗髮精", "洗髮乳", "洗面乳", "洗面奶", "乳液", "精華",
|
|||
|
|
"防曬", "卸妝", "潔面", "身體乳", "洗手乳", "清潔",
|
|||
|
|
} {
|
|||
|
|
if strings.Contains(combined, hint) {
|
|||
|
|
return hint
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return ""
|
|||
|
|
}
|