feat: simplify SOP to immediate-effect configuration

This commit is contained in:
Eric 1549169735@qq.com
2026-08-18 16:27:02 +08:00
parent c5ab886b70
commit 8de48fb05e
150 changed files with 6764 additions and 1626 deletions

View File

@@ -0,0 +1,320 @@
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])
}