fix(brand-questions): accept generic search terms in lexicon validation
Deployment Config CI / Deployment Config (push) Successful in 27s
Frontend CI / Frontend (push) Successful in 2m53s
Backend CI / Backend (push) Successful in 16m53s
Desktop Client Build / Resolve Build Metadata (push) Waiting to run
Desktop Client Build / Build Desktop Client (push) Blocked by required conditions
Desktop Client Build / Publish Client Artifacts to NAS (push) Has been cancelled
Deployment Config CI / Deployment Config (push) Successful in 27s
Frontend CI / Frontend (push) Successful in 2m53s
Backend CI / Backend (push) Successful in 16m53s
Desktop Client Build / Resolve Build Metadata (push) Waiting to run
Desktop Client Build / Build Desktop Client (push) Blocked by required conditions
Desktop Client Build / Publish Client Artifacts to NAS (push) Has been cancelled
Search terms like "全屋定制" or "GEO排名优化" were rejected as invalid because both the Vue candidate validator and the Go question_metadata classifier required interrogative cues (什么/如何/为什么/...). Lexicon entries are search terms, not questions, so require only letters or digits plus the existing 4-char minimum. Rename helpers to searchTermLooksValid/questionLooksValid to reflect the looser semantics, and relabel the navigation entry from "公司和词库" to "品牌和词库" to match the actual domain.
This commit is contained in:
@@ -79,7 +79,7 @@ const zhCN = {
|
|||||||
media: "媒体管理",
|
media: "媒体管理",
|
||||||
publishManagement: "发文管理",
|
publishManagement: "发文管理",
|
||||||
brandManagement: "品牌管理",
|
brandManagement: "品牌管理",
|
||||||
brands: "公司和词库",
|
brands: "品牌和词库",
|
||||||
tracking: "数据追踪",
|
tracking: "数据追踪",
|
||||||
trackingDetail: "数据详情",
|
trackingDetail: "数据详情",
|
||||||
contentManagement: "内容管理",
|
contentManagement: "内容管理",
|
||||||
@@ -222,7 +222,7 @@ const zhCN = {
|
|||||||
description: "查看桌面端发布任务、外链、账号工作台入口和重发状态。",
|
description: "查看桌面端发布任务、外链、账号工作台入口和重发状态。",
|
||||||
},
|
},
|
||||||
brands: {
|
brands: {
|
||||||
title: "公司和词库",
|
title: "品牌和词库",
|
||||||
description: "围绕品牌维护关键词库(搜索词)和竞品信息。",
|
description: "围绕品牌维护关键词库(搜索词)和竞品信息。",
|
||||||
},
|
},
|
||||||
brandQuestionCreate: {
|
brandQuestionCreate: {
|
||||||
@@ -1275,7 +1275,7 @@ const zhCN = {
|
|||||||
},
|
},
|
||||||
brands: {
|
brands: {
|
||||||
eyebrow: "Company & Lexicon",
|
eyebrow: "Company & Lexicon",
|
||||||
title: "公司和词库",
|
title: "品牌和词库",
|
||||||
description: "通过品牌名称和品牌相关信息维护关键词库(搜索词)和竞品资产。",
|
description: "通过品牌名称和品牌相关信息维护关键词库(搜索词)和竞品资产。",
|
||||||
railTitle: "品牌库",
|
railTitle: "品牌库",
|
||||||
selectBrand: "选择品牌后即可继续维护关键词库(搜索词)。",
|
selectBrand: "选择品牌后即可继续维护关键词库(搜索词)。",
|
||||||
|
|||||||
@@ -506,12 +506,12 @@ function normalizeSearchTermText(value: string): string {
|
|||||||
.replace(/\s+/g, '')
|
.replace(/\s+/g, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
function questionLooksLikeQuestion(value: string): boolean {
|
function searchTermLooksValid(value: string): boolean {
|
||||||
const text = normalizeSearchTermText(value)
|
const text = normalizeSearchTermText(value)
|
||||||
if (text.length < 4) {
|
if (text.length < 4) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return /什么|如何|怎么|哪|为什么|是否|能不能|适合|区别|对比|推荐/.test(text)
|
return /[\p{L}\p{N}]/u.test(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toCandidateRows(candidates: QuestionCandidate[]): CandidateRow[] {
|
function toCandidateRows(candidates: QuestionCandidate[]): CandidateRow[] {
|
||||||
@@ -519,7 +519,7 @@ function toCandidateRows(candidates: QuestionCandidate[]): CandidateRow[] {
|
|||||||
.map((candidate, index) => {
|
.map((candidate, index) => {
|
||||||
const text = normalizeSearchTermText(candidate.text)
|
const text = normalizeSearchTermText(candidate.text)
|
||||||
const tooShort = text.length < 4
|
const tooShort = text.length < 4
|
||||||
const invalid = !questionLooksLikeQuestion(text)
|
const invalid = !searchTermLooksValid(text)
|
||||||
return {
|
return {
|
||||||
...candidate,
|
...candidate,
|
||||||
text,
|
text,
|
||||||
@@ -545,7 +545,7 @@ function buildLocalCandidates(texts: string[], source: QuestionSource): Candidat
|
|||||||
seen.add(key)
|
seen.add(key)
|
||||||
const tooShort = text.length < 4
|
const tooShort = text.length < 4
|
||||||
const duplicate = existingQuestionSet.value.has(key) || duplicateInBatch
|
const duplicate = existingQuestionSet.value.has(key) || duplicateInBatch
|
||||||
const invalid = !questionLooksLikeQuestion(text)
|
const invalid = !searchTermLooksValid(text)
|
||||||
rows.push({
|
rows.push({
|
||||||
id: `${Date.now()}-${rows.length}-${text}`,
|
id: `${Date.now()}-${rows.length}-${text}`,
|
||||||
text,
|
text,
|
||||||
@@ -682,7 +682,7 @@ function refreshCandidate(row: CandidateRow): void {
|
|||||||
)
|
)
|
||||||
row.too_short = row.text.length < 4
|
row.too_short = row.text.length < 4
|
||||||
row.duplicate = Boolean(key) && (existingQuestionSet.value.has(key) || duplicateInBatch)
|
row.duplicate = Boolean(key) && (existingQuestionSet.value.has(key) || duplicateInBatch)
|
||||||
row.suggest_skip = row.too_short || row.duplicate || !questionLooksLikeQuestion(row.text)
|
row.suggest_skip = row.too_short || row.duplicate || !searchTermLooksValid(row.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshAllCandidates(): void {
|
function refreshAllCandidates(): void {
|
||||||
|
|||||||
@@ -123,17 +123,14 @@ func questionLooksValid(text string) bool {
|
|||||||
if utf8.RuneCountInString(trimmed) < 4 {
|
if utf8.RuneCountInString(trimmed) < 4 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
hasLetterOrNumber := false
|
hasSearchTermChar := false
|
||||||
for _, r := range trimmed {
|
for _, r := range trimmed {
|
||||||
if unicode.IsLetter(r) || unicode.IsNumber(r) {
|
if unicode.IsLetter(r) || unicode.IsNumber(r) {
|
||||||
hasLetterOrNumber = true
|
hasSearchTermChar = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !hasLetterOrNumber {
|
return hasSearchTermChar
|
||||||
return false
|
|
||||||
}
|
|
||||||
return containsAny(strings.ToLower(trimmed), []string{"?", "?", "什么", "如何", "怎么", "哪", "为什么", "是否", "能不能", "适合", "区别", "对比", "推荐"})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func containsAny(value string, needles []string) bool {
|
func containsAny(value string, needles []string) bool {
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestQuestionLooksValidAcceptsGenericSearchTerms(t *testing.T) {
|
||||||
|
tests := []string{
|
||||||
|
"全屋定制",
|
||||||
|
"GEO排名优化",
|
||||||
|
"AI搜索排名",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, text := range tests {
|
||||||
|
if !questionLooksValid(text) {
|
||||||
|
t.Fatalf("questionLooksValid(%q) = false, want true", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildCandidateKeepsGenericSearchTermSavable(t *testing.T) {
|
||||||
|
candidate := buildCandidate(
|
||||||
|
"全屋定制",
|
||||||
|
QuestionSourceManual,
|
||||||
|
false,
|
||||||
|
map[string]struct{}{},
|
||||||
|
&questionBrandContext{BrandName: "安徽海翔家居用品销售有限公司"},
|
||||||
|
)
|
||||||
|
|
||||||
|
if candidate.TooShort {
|
||||||
|
t.Fatal("expected generic search term not to be marked too short")
|
||||||
|
}
|
||||||
|
if candidate.SuggestSkip {
|
||||||
|
t.Fatal("expected generic search term not to be suggested for skip")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuestionLooksValidRejectsEmptyOrTooShortTerms(t *testing.T) {
|
||||||
|
tests := []string{
|
||||||
|
"",
|
||||||
|
"!?。",
|
||||||
|
"AI",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, text := range tests {
|
||||||
|
if questionLooksValid(text) {
|
||||||
|
t.Fatalf("questionLooksValid(%q) = true, want false", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user