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 TargetAudience 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), TargetAudience: strings.TrimSpace(brand.TargetAudience), MatchTags: matchTagsFromBrand(brand), Questions: append([]string{}, brand.ResearchMap.Questions...), Pillars: append([]string{}, brand.ResearchMap.Pillars...), PatrolKeywords: append([]string{}, brand.ResearchMap.PatrolKeywords...), } } // OverlayResearchMap copies topic-level research map fields onto patrol input. func OverlayResearchMap(in PatrolTagInput, audienceSummary string, questions, pillars, patrolKeywords []string) PatrolTagInput { if strings.TrimSpace(audienceSummary) != "" { in.AudienceSummary = strings.TrimSpace(audienceSummary) } if len(questions) > 0 { in.Questions = append([]string{}, questions...) } if len(pillars) > 0 { in.Pillars = append([]string{}, pillars...) } if len(patrolKeywords) > 0 { in.PatrolKeywords = append([]string{}, patrolKeywords...) } return in } 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 } var patrolCoreValueTokens = []string{ "敏感", "無香", "抗敏", "低敏", "香精", "香料", "香味", "過敏", "皮膚", "化療", "癌症", "乳癌", "荷爾蒙", "標靶", "病友", } func primaryPatrolValueAnchor(in PatrolTagInput) string { blob := strings.ToLower(strings.TrimSpace(in.ProductName + " " + in.ProductFeatures + " " + strings.Join(in.MatchTags, " ") + " " + strings.Join(in.Pillars, " ") + " " + in.AudienceSummary + " " + in.TargetAudience)) priority := []string{"無香", "抗敏", "敏感", "化療", "癌症", "乳癌", "荷爾蒙", "過敏", "香味", "香精"} for _, token := range priority { if strings.Contains(blob, token) { return token } } for _, token := range patrolCoreValueTokens { if strings.Contains(blob, token) { return token } } return "" } func productCategoryHint(productName, features string) string { combined := productName + " " + features for _, hint := range []string{ "洗衣精", "洗衣粉", "衣物", "洗碗精", "洗碗機", "碗盤", "沐浴乳", "沐浴露", "洗髮精", "洗髮乳", "洗面乳", "洗面奶", "乳液", "精華", "防曬", "卸妝", "潔面", "身體乳", "洗手乳", "清潔", } { if strings.Contains(combined, hint) { return hint } } return "" }