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:
2026-05-12 21:53:36 +08:00
parent 77d542c282
commit 37b0b32327
27 changed files with 4619 additions and 908 deletions
+42 -4
View File
@@ -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) {
+1 -1
View File
@@ -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 {