2026-06-26 08:37:04 +00:00
|
|
|
package placement
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"strings"
|
2026-07-01 08:42:51 +00:00
|
|
|
|
|
|
|
|
brandentity "haixun-backend/internal/model/brand/domain/entity"
|
2026-06-26 08:37:04 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func ScoreProductForTag(searchTag string, matchTags []string) int {
|
|
|
|
|
tag := strings.TrimSpace(strings.ToLower(searchTag))
|
|
|
|
|
if tag == "" || len(matchTags) == 0 {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
best := 0
|
|
|
|
|
for _, raw := range matchTags {
|
|
|
|
|
candidate := strings.TrimSpace(strings.ToLower(raw))
|
|
|
|
|
if candidate == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
switch {
|
|
|
|
|
case tag == candidate:
|
|
|
|
|
best = maxInt(best, 100)
|
|
|
|
|
case strings.Contains(tag, candidate) || strings.Contains(candidate, tag):
|
|
|
|
|
best = maxInt(best, 70)
|
|
|
|
|
default:
|
|
|
|
|
tagWords := strings.Fields(tag)
|
|
|
|
|
candWords := strings.Fields(candidate)
|
|
|
|
|
overlap := 0
|
|
|
|
|
for _, w := range tagWords {
|
|
|
|
|
for _, c := range candWords {
|
|
|
|
|
if strings.Contains(c, w) || strings.Contains(w, c) {
|
|
|
|
|
overlap++
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if overlap > 0 {
|
|
|
|
|
best = maxInt(best, 40+overlap*10)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return best
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 08:42:51 +00:00
|
|
|
// ScoreProductForPost ranks a SKU against the search tag and post body.
|
|
|
|
|
func ScoreProductForPost(productLabel string, matchTags []string, searchTag, postText string) int {
|
|
|
|
|
tagScore := ScoreProductForTag(searchTag, matchTags)
|
|
|
|
|
if tagScore == 0 && strings.TrimSpace(productLabel) != "" {
|
|
|
|
|
tagScore = ScoreProductForTag(searchTag, []string{productLabel})
|
|
|
|
|
}
|
|
|
|
|
postScore := ScoreProductForTag(postText, matchTags)
|
|
|
|
|
if postScore == 0 && strings.TrimSpace(productLabel) != "" {
|
|
|
|
|
postScore = ScoreProductForTag(postText, []string{productLabel})
|
|
|
|
|
}
|
|
|
|
|
for _, token := range categoryTokens(productLabel, matchTags) {
|
|
|
|
|
if strings.Contains(strings.ToLower(postText), token) {
|
|
|
|
|
postScore = maxInt(postScore, 78)
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(strings.ToLower(searchTag), token) {
|
|
|
|
|
tagScore = maxInt(tagScore, 82)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
switch {
|
|
|
|
|
case tagScore > 0 && postScore > 0:
|
|
|
|
|
return (tagScore*2 + postScore*3) / 5
|
|
|
|
|
case postScore > 0:
|
|
|
|
|
return postScore
|
|
|
|
|
default:
|
|
|
|
|
return tagScore
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RecommendProductForPost picks the best SKU for a single post.
|
|
|
|
|
func RecommendProductForPost(products []brandentity.Product, searchTag, postText, defaultProductID string) *brandentity.Product {
|
|
|
|
|
if len(products) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
bestScore := 0
|
|
|
|
|
var best *brandentity.Product
|
|
|
|
|
for i := range products {
|
|
|
|
|
score := ScoreProductForPost(products[i].Label, products[i].MatchTags, searchTag, postText)
|
|
|
|
|
if score > bestScore {
|
|
|
|
|
bestScore = score
|
|
|
|
|
best = &products[i]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if best != nil && bestScore >= 55 {
|
|
|
|
|
return best
|
|
|
|
|
}
|
|
|
|
|
return RecommendProduct(products, searchTag, defaultProductID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func categoryTokens(productLabel string, matchTags []string) []string {
|
|
|
|
|
blob := strings.ToLower(strings.TrimSpace(productLabel + " " + strings.Join(matchTags, " ")))
|
|
|
|
|
tokens := []string{
|
|
|
|
|
"洗衣精", "洗衣粉", "洗衣劑", "洗衣液",
|
|
|
|
|
"沐浴乳", "沐浴露",
|
|
|
|
|
"洗髮精", "洗髮乳",
|
|
|
|
|
"洗碗精", "洗碗劑",
|
|
|
|
|
}
|
|
|
|
|
out := make([]string, 0, 4)
|
|
|
|
|
for _, token := range tokens {
|
|
|
|
|
if strings.Contains(blob, token) {
|
|
|
|
|
out = append(out, token)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 08:37:04 +00:00
|
|
|
func maxInt(a, b int) int {
|
|
|
|
|
if a > b {
|
|
|
|
|
return a
|
|
|
|
|
}
|
|
|
|
|
return b
|
|
|
|
|
}
|