feat(admin-brand): add question expansion service and related functionality
- Implemented QuestionExpansionService for generating and materializing questions based on combinations and AI distillation. - Added question metadata classification logic to infer intent and layer of questions. - Created API handlers for question expansion operations including combination preview, AI distillation, metadata classification, and materialization. - Introduced database migrations to support new question-related fields and constraints in brand_questions and brand_keywords tables. - Added caching mechanism for AI distillation results to improve performance. - Defined JSON schemas for question distillation responses to ensure data integrity.
This commit is contained in:
@@ -237,10 +237,11 @@ func (p MembershipPlanConfig) QuotaPolicyJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
type BrandLibraryConfig struct {
|
||||
FreeBrandLimit int `mapstructure:"free_brand_limit"`
|
||||
PaidBrandLimit int `mapstructure:"paid_brand_limit"`
|
||||
MaxKeywords int `mapstructure:"max_keywords"`
|
||||
MaxQuestionsPerKeyword int `mapstructure:"max_questions_per_keyword"`
|
||||
FreeBrandLimit int `mapstructure:"free_brand_limit"`
|
||||
PaidBrandLimit int `mapstructure:"paid_brand_limit"`
|
||||
MaxKeywords int `mapstructure:"max_keywords"`
|
||||
MaxQuestionsPerKeyword int `mapstructure:"max_questions_per_keyword"`
|
||||
QuestionLimitsByPlan map[string]int `mapstructure:"question_limits_by_plan"`
|
||||
}
|
||||
|
||||
func (c BrandLibraryConfig) BrandLimitForPlan(planCode string) int {
|
||||
@@ -250,6 +251,20 @@ func (c BrandLibraryConfig) BrandLimitForPlan(planCode string) int {
|
||||
return c.PaidBrandLimit
|
||||
}
|
||||
|
||||
func (c BrandLibraryConfig) QuestionLimitForPlan(planCode string) int {
|
||||
limits := c.QuestionLimitsByPlan
|
||||
normalizedPlan := strings.ToLower(strings.TrimSpace(planCode))
|
||||
if limits != nil {
|
||||
if value := limits[normalizedPlan]; value > 0 {
|
||||
return value
|
||||
}
|
||||
if value := limits["default"]; value > 0 {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return 25
|
||||
}
|
||||
|
||||
type QdrantConfig struct {
|
||||
URL string `mapstructure:"url"`
|
||||
APIKey string `mapstructure:"api_key"`
|
||||
@@ -1173,6 +1188,29 @@ func normalizeBrandLibraryConfig(cfg *BrandLibraryConfig) {
|
||||
if cfg.MaxQuestionsPerKeyword <= 0 {
|
||||
cfg.MaxQuestionsPerKeyword = 5
|
||||
}
|
||||
if cfg.QuestionLimitsByPlan == nil {
|
||||
cfg.QuestionLimitsByPlan = map[string]int{}
|
||||
}
|
||||
normalized := make(map[string]int, len(cfg.QuestionLimitsByPlan)+4)
|
||||
for key, value := range cfg.QuestionLimitsByPlan {
|
||||
trimmed := strings.ToLower(strings.TrimSpace(key))
|
||||
if trimmed != "" && value > 0 {
|
||||
normalized[trimmed] = value
|
||||
}
|
||||
}
|
||||
if normalized["default"] <= 0 {
|
||||
normalized["default"] = 25
|
||||
}
|
||||
if normalized["free"] <= 0 {
|
||||
normalized["free"] = 5
|
||||
}
|
||||
if normalized["plus"] <= 0 {
|
||||
normalized["plus"] = 25
|
||||
}
|
||||
if normalized["pro"] <= 0 {
|
||||
normalized["pro"] = 50
|
||||
}
|
||||
cfg.QuestionLimitsByPlan = normalized
|
||||
}
|
||||
|
||||
func normalizeMembershipConfig(cfg *MembershipConfig) {
|
||||
|
||||
@@ -62,7 +62,7 @@ func Diff(previous, current *Config) []FieldChange {
|
||||
if !reflect.DeepEqual(previous.Membership, current.Membership) {
|
||||
addChange("membership", true)
|
||||
}
|
||||
if previous.BrandLibrary != current.BrandLibrary {
|
||||
if !reflect.DeepEqual(previous.BrandLibrary, current.BrandLibrary) {
|
||||
addChange("brand_library", true)
|
||||
}
|
||||
if previous.Qdrant != current.Qdrant {
|
||||
|
||||
@@ -175,10 +175,14 @@ var routeDocs = map[string]routeDoc{
|
||||
"PUT /api/tenant/brands/:id/keywords/:kid": {"更新关键词", "修改关键词文本/分组。"},
|
||||
"DELETE /api/tenant/brands/:id/keywords/:kid": {"删除关键词", "删除某条关键词。"},
|
||||
|
||||
"GET /api/tenant/brands/:id/questions": {"品牌问题列表", "返回品牌下的监控问题,可按 keyword_id 过滤。"},
|
||||
"POST /api/tenant/brands/:id/questions": {"新增监控问题", "为品牌添加一条 GEO 监控问题。"},
|
||||
"PUT /api/tenant/brands/:id/questions/:qid": {"更新监控问题", "修改问题文本或所属关键词。"},
|
||||
"DELETE /api/tenant/brands/:id/questions/:qid": {"删除监控问题", "删除某条监控问题。"},
|
||||
"GET /api/tenant/brands/:id/questions": {"品牌问题列表", "返回品牌下的监控问题,可按 keyword_id 过滤。"},
|
||||
"POST /api/tenant/brands/:id/questions": {"新增监控问题", "为品牌添加一条 GEO 监控问题。"},
|
||||
"POST /api/tenant/brands/:id/questions/combination-preview": {"拓词工具预览", "按地域词、前缀词、核心词、行业词、后缀词组合生成问题候选,仅预览不入库。"},
|
||||
"POST /api/tenant/brands/:id/questions/ai-distill": {"AI 扩展问题", "围绕品牌和主题生成问题候选,使用结构化输出并按 AI 点计费。"},
|
||||
"POST /api/tenant/brands/:id/questions/classify-metadata": {"问题元数据分类", "批量为问题文本推断 layer 和 intent 元数据。"},
|
||||
"POST /api/tenant/brands/:id/questions/materialize": {"保存问题候选", "将用户选中的问题候选保存到当前品牌问题集,执行配额、去重和审计。"},
|
||||
"PUT /api/tenant/brands/:id/questions/:qid": {"更新监控问题", "修改问题文本或所属关键词。"},
|
||||
"DELETE /api/tenant/brands/:id/questions/:qid": {"删除监控问题", "删除某条监控问题。"},
|
||||
|
||||
"GET /api/tenant/brands/:id/competitors": {"竞品列表", "返回品牌下登记的竞品。"},
|
||||
"POST /api/tenant/brands/:id/competitors": {"新增竞品", "为品牌添加竞品记录。"},
|
||||
|
||||
Reference in New Issue
Block a user