package main import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "os" "sort" "strings" "git.iwork-ai.com/xdc/iqudo-top1/internal/model" "github.com/xuri/excelize/v2" "gorm.io/datatypes" "gorm.io/gorm" ) const petKnowledgeSource = "症状-疾病-方案.xlsx" const petScriptSource = "症状-疾病-话术.json" func resetPetKnowledge(tx *gorm.DB, tenantID, scenarioID uint64) error { var ids []uint64 if err := tx.Model(&model.KnowledgeItem{}).Where("tenant_id = ? AND scenario_id = ?", tenantID, scenarioID).Pluck("id", &ids).Error; err != nil { return fmt.Errorf("list old pet knowledge: %w", err) } if len(ids) > 0 { if err := tx.Where("tenant_id = ? AND scenario_id = ? AND (from_knowledge_id IN ? OR to_knowledge_id IN ?)", tenantID, scenarioID, ids, ids).Delete(&model.KnowledgeRelation{}).Error; err != nil { return fmt.Errorf("delete old pet knowledge relations: %w", err) } if err := tx.Where("tenant_id = ? AND scenario_id = ? AND id IN ?", tenantID, scenarioID, ids).Delete(&model.KnowledgeItem{}).Error; err != nil { return fmt.Errorf("delete old pet knowledge items: %w", err) } } return nil } type petScriptDisease struct { Plans []struct { Plan string `json:"plan"` Products []struct { SKUCode string `json:"sku_code"` ProductName string `json:"product_name"` } `json:"products"` } `json:"plans"` Combinations []struct { Name string `json:"name"` Weight int `json:"weight"` Products []struct { SKUCode string `json:"sku_code"` ProductName string `json:"product_name"` } `json:"products"` Script string `json:"script"` } `json:"combinations"` } func importPetScripts(tx *gorm.DB, tenantID, scenarioID uint64, filename string) error { raw, err := os.ReadFile(filename) if err != nil { return fmt.Errorf("read pet scripts %s: %w", filename, err) } var source map[string]map[string]petScriptDisease if err := json.Unmarshal(raw, &source); err != nil { return fmt.Errorf("parse pet scripts %s: %w", filename, err) } for symptom, diseases := range source { for disease, definition := range diseases { diseaseKey := petKnowledgeKey("disease", disease) if err := upsertPetScriptItems(tx, tenantID, scenarioID, symptom, disease, diseaseKey, definition); err != nil { return err } } } return nil } func upsertPetScriptItems(tx *gorm.DB, tenantID, scenarioID uint64, symptom, disease, diseaseKey string, definition petScriptDisease) error { symptomKey := petKnowledgeKey("symptom", symptom) if err := upsertPetKnowledgeItem(tx, tenantID, scenarioID, symptomKey, symptom, "symptom", []byte(`{"source":"`+petScriptSource+`"}`)); err != nil { return err } if err := upsertPetKnowledgeItem(tx, tenantID, scenarioID, diseaseKey, disease, "disease", []byte(`{"source":"`+petScriptSource+`"}`)); err != nil { return err } if err := upsertPetRelation(tx, scenarioID, symptomKey, "possible_disease", diseaseKey, 3000); err != nil { return err } for index, combination := range definition.Combinations { copyName := fmt.Sprintf("%s-%s话术", disease, combination.Name) copyKey := petKnowledgeKey("copy", "json_"+symptom+"_"+disease+"_"+combination.Name) content, _ := json.Marshal(map[string]interface{}{"source": petScriptSource, "template": strings.ReplaceAll(combination.Script, "{{purchased_products}}", "{{input.product_names}}"), "combination": combination.Name, "weight": combination.Weight}) if err := upsertPetKnowledgeItem(tx, tenantID, scenarioID, copyKey, copyName, "copy", content); err != nil { return err } if err := upsertPetRelation(tx, scenarioID, diseaseKey, "recommended_copy", copyKey, 3000+index); err != nil { return err } for productIndex, product := range combination.Products { if strings.TrimSpace(product.SKUCode) == "" { continue } productKey := petKnowledgeKey("product", product.SKUCode) content, _ := json.Marshal(map[string]interface{}{"source": petScriptSource, "sku_code": product.SKUCode, "product_name": product.ProductName}) if err := upsertPetKnowledgeItem(tx, tenantID, scenarioID, productKey, product.ProductName, "product", content); err != nil { return err } if err := upsertPetRelation(tx, scenarioID, diseaseKey, "recommended_product", productKey, 3400+index*100+productIndex); err != nil { return err } } } for index, plan := range definition.Plans { if strings.TrimSpace(plan.Plan) == "" { continue } planKey := petKnowledgeKey("plan", plan.Plan) if err := upsertPetKnowledgeItem(tx, tenantID, scenarioID, planKey, plan.Plan, "plan", []byte(`{"source":"`+petScriptSource+`"}`)); err != nil { return err } if err := upsertPetRelation(tx, scenarioID, diseaseKey, "recommended_plan", planKey, 3200+index); err != nil { return err } for productIndex, product := range plan.Products { if strings.TrimSpace(product.SKUCode) == "" { continue } productKey := petKnowledgeKey("product", product.SKUCode) content, _ := json.Marshal(map[string]interface{}{"source": petScriptSource, "sku_code": product.SKUCode, "product_name": product.ProductName}) if err := upsertPetKnowledgeItem(tx, tenantID, scenarioID, productKey, product.ProductName, "product", content); err != nil { return err } if err := upsertPetRelation(tx, scenarioID, diseaseKey, "recommended_product", productKey, 3600+index*100+productIndex); err != nil { return err } } } return nil } func upsertPetKnowledgeItem(tx *gorm.DB, tenantID, scenarioID uint64, key, name, typeName string, content []byte) error { row := model.KnowledgeItem{TenantID: tenantID, ScenarioID: scenarioID, ItemKey: key, Name: name, Type: typeName, Content: datatypes.JSON(content), Status: "active", SortOrder: 3000} return tx.Where("tenant_id = ? AND scenario_id = ? AND item_key = ?", tenantID, scenarioID, key).Assign(row).FirstOrCreate(&row).Error } func upsertPetRelation(tx *gorm.DB, scenarioID uint64, fromKey, relation, toKey string, order int) error { var from, to model.KnowledgeItem if err := tx.Where("scenario_id = ? AND item_key = ?", scenarioID, fromKey).First(&from).Error; err != nil { return fmt.Errorf("find script relation source %s: %w", fromKey, err) } if err := tx.Where("scenario_id = ? AND item_key = ?", scenarioID, toKey).First(&to).Error; err != nil { return fmt.Errorf("find script relation target %s: %w", toKey, err) } row := model.KnowledgeRelation{TenantID: from.TenantID, ScenarioID: scenarioID, FromKnowledgeID: from.ID, RelationType: relation, ToKnowledgeID: to.ID, Condition: datatypes.JSON([]byte(`{}`)), SortOrder: order} return tx.Where("scenario_id = ? AND from_knowledge_id = ? AND relation_type = ? AND to_knowledge_id = ?", scenarioID, from.ID, relation, to.ID).Assign(row).FirstOrCreate(&row).Error } type petKnowledgeRow struct { Symptom string Disease string Plan string SKUCode string ProductName string } func importPetKnowledge(tx *gorm.DB, tenantID, scenarioID uint64, filename string) error { rows, err := readPetKnowledge(filename) if err != nil { return err } return upsertPetKnowledge(tx, tenantID, scenarioID, rows) } func readPetKnowledge(filename string) ([]petKnowledgeRow, error) { book, err := excelize.OpenFile(filename) if err != nil { return nil, fmt.Errorf("open pet knowledge %s: %w", filename, err) } defer book.Close() sheets := book.GetSheetList() if len(sheets) == 0 { return nil, fmt.Errorf("pet knowledge %s has no worksheet", filename) } iterator, err := book.Rows(sheets[0]) if err != nil { return nil, fmt.Errorf("read pet knowledge worksheet: %w", err) } defer iterator.Close() columns := map[string]int{} result := make([]petKnowledgeRow, 0) rowNumber := 0 for iterator.Next() { rowNumber++ values, err := iterator.Columns() if err != nil { return nil, fmt.Errorf("read pet knowledge row %d: %w", rowNumber, err) } if rowNumber == 1 { for index, value := range values { columns[strings.TrimSpace(value)] = index } for _, required := range []string{"症状", "疾病", "方案", "SKU_CODE", "商品标题"} { if _, ok := columns[required]; !ok { return nil, fmt.Errorf("pet knowledge is missing column %q", required) } } continue } row := petKnowledgeRow{ Symptom: excelValue(values, columns["症状"]), Disease: excelValue(values, columns["疾病"]), Plan: excelValue(values, columns["方案"]), SKUCode: excelValue(values, columns["SKU_CODE"]), ProductName: excelValue(values, columns["商品标题"]), } if row.Symptom == "" && row.Disease == "" && row.Plan == "" { continue } if row.Symptom == "" || row.Disease == "" { return nil, fmt.Errorf("pet knowledge row %d must contain 场景 and 分型", rowNumber) } result = append(result, row) } if err := iterator.Error(); err != nil { return nil, fmt.Errorf("iterate pet knowledge: %w", err) } if len(result) == 0 { return nil, fmt.Errorf("pet knowledge contains no data") } return result, nil } func excelValue(values []string, index int) string { if index >= len(values) { return "" } return strings.TrimSpace(values[index]) } func upsertPetKnowledge(tx *gorm.DB, tenantID, scenarioID uint64, rows []petKnowledgeRow) error { type itemDefinition struct { name, typeName string content map[string]interface{} } items := map[string]itemDefinition{} type relationDefinition struct{ from, relation, to string } relations := map[relationDefinition]bool{} for _, row := range rows { symptomKey := petKnowledgeKey("symptom", row.Symptom) diseaseKey := petKnowledgeKey("disease", row.Disease) items[symptomKey] = itemDefinition{name: row.Symptom, typeName: "symptom"} items[diseaseKey] = itemDefinition{name: row.Disease, typeName: "disease"} relations[relationDefinition{symptomKey, "possible_disease", diseaseKey}] = true for index, template := range []string{ "结合客户描述,目前更符合“%s”这一分型。建议先向客户说明判断依据,再介绍对应方案。", "针对“%s”,这边建议按下面的方案进行护理和商品搭配;如症状持续或加重,应及时就医。", } { copyName := fmt.Sprintf("%s推荐话术%d", row.Disease, index+1) copyKey := petKnowledgeKey("copy", copyName) items[copyKey] = itemDefinition{name: copyName, typeName: "copy", content: map[string]interface{}{"template": fmt.Sprintf(template, row.Disease)}} relations[relationDefinition{diseaseKey, "recommended_copy", copyKey}] = true } if row.Plan != "" { planKey := petKnowledgeKey("plan", row.Plan) items[planKey] = itemDefinition{name: row.Plan, typeName: "plan"} relations[relationDefinition{diseaseKey, "recommended_plan", planKey}] = true // The current SOP view expands one relation level from a symptom. relations[relationDefinition{symptomKey, "recommended_plan", planKey}] = true } if row.SKUCode != "" { productName := row.ProductName if productName == "" { productName = row.SKUCode } productKey := petKnowledgeKey("product", row.SKUCode) items[productKey] = itemDefinition{name: productName, typeName: "product", content: map[string]interface{}{"sku_code": row.SKUCode, "product_name": productName}} relations[relationDefinition{diseaseKey, "recommended_product", productKey}] = true } } keys := make([]string, 0, len(items)) for key := range items { keys = append(keys, key) } sort.Strings(keys) ids := make(map[string]uint64, len(keys)) for order, key := range keys { definition := items[key] payload := map[string]interface{}{"source": petKnowledgeSource} for name, value := range definition.content { payload[name] = value } content, _ := json.Marshal(payload) row := model.KnowledgeItem{TenantID: tenantID, ScenarioID: scenarioID, ItemKey: key, Name: definition.name, Type: definition.typeName, Content: datatypes.JSON(content), Status: "active", SortOrder: 1000 + order} if err := tx.Where("tenant_id = ? AND scenario_id = ? AND item_key = ?", tenantID, scenarioID, key).Assign(row).FirstOrCreate(&row).Error; err != nil { return fmt.Errorf("upsert imported knowledge item %s: %w", key, err) } ids[key] = row.ID } relationList := make([]relationDefinition, 0, len(relations)) for relation := range relations { relationList = append(relationList, relation) } sort.Slice(relationList, func(i, j int) bool { left, right := relationList[i], relationList[j] return left.from+left.relation+left.to < right.from+right.relation+right.to }) for order, definition := range relationList { row := model.KnowledgeRelation{TenantID: tenantID, ScenarioID: scenarioID, FromKnowledgeID: ids[definition.from], RelationType: definition.relation, ToKnowledgeID: ids[definition.to], Condition: datatypes.JSON([]byte(`{}`)), SortOrder: 1000 + order} if err := tx.Where("scenario_id = ? AND from_knowledge_id = ? AND relation_type = ? AND to_knowledge_id = ?", scenarioID, row.FromKnowledgeID, row.RelationType, row.ToKnowledgeID).Assign(row).FirstOrCreate(&row).Error; err != nil { return fmt.Errorf("upsert imported knowledge relation: %w", err) } } return nil } func petKnowledgeKey(typeName, name string) string { digest := sha256.Sum256([]byte(typeName + "\x00" + strings.TrimSpace(name))) return "xlsx_" + typeName + "_" + hex.EncodeToString(digest[:6]) }