603 lines
24 KiB
Go
603 lines
24 KiB
Go
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"sort"
|
||
"strings"
|
||
|
||
"git.iwork-ai.com/xdc/iqudo-top1/internal/scriptkit"
|
||
"github.com/xuri/excelize/v2"
|
||
)
|
||
|
||
// Data sources for the script package.
|
||
const petScriptSource = "症状-疾病-话术.json"
|
||
const petQuestionSource = "症状确认话术.json"
|
||
const petSmallCategorySource = "症状小类-大类.json"
|
||
const petSmallAskScriptSource = "症状小类询问话术.json"
|
||
const petPreGuideSource = "症状确认前引导话术.json"
|
||
const petClassificationSource = "症状分类.json"
|
||
const petKnowledgeSource = "症状-疾病-方案.xlsx"
|
||
|
||
type petScriptEntry struct {
|
||
Title string `json:"title"`
|
||
Template string `json:"template"`
|
||
}
|
||
|
||
type petClassification struct {
|
||
Symptoms []struct {
|
||
Type string `json:"type"`
|
||
Subs []string `json:"subs"`
|
||
} `json:"symptoms"`
|
||
Diagnosis []struct {
|
||
Type string `json:"type"`
|
||
Subs []struct {
|
||
SubType string `json:"subType"`
|
||
} `json:"subs"`
|
||
} `json:"diagnosis"`
|
||
}
|
||
|
||
type petCombination 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"`
|
||
}
|
||
|
||
type petScriptDisease struct {
|
||
Combinations []petCombination `json:"combinations"`
|
||
}
|
||
|
||
type petKnowledgeRow struct {
|
||
Symptom string
|
||
Disease string
|
||
SKUCode string
|
||
ProductName string
|
||
}
|
||
|
||
// petQuestionBank is the customer-facing copy bank for the script package.
|
||
// Every question lives in the JSON data file, not in Go code.
|
||
type petQuestionBank struct {
|
||
OpeningGreeting string `json:"opening_greeting"`
|
||
DiagnosisFallback string `json:"diagnosis_fallback"`
|
||
RecommendFallback string `json:"recommend_fallback"`
|
||
SymptomScreeningTemplate string `json:"symptom_screening_template"`
|
||
SymptomConfirmOptions struct {
|
||
Yes string `json:"yes"`
|
||
No string `json:"no"`
|
||
} `json:"symptom_confirm_options"`
|
||
DiseaseConfirmOptions struct {
|
||
Yes string `json:"yes"`
|
||
No string `json:"no"`
|
||
} `json:"disease_confirm_options"`
|
||
Symptoms map[string]petSymptomQuestions `json:"symptoms"`
|
||
|
||
DiseaseConfirmTemplate string `json:"disease_confirm_template"`
|
||
DiseaseConfirmedCopyTemplate string `json:"disease_confirmed_copy_template"`
|
||
DiseasePlanIntro string `json:"disease_plan_intro"`
|
||
DiseaseCareNote string `json:"disease_care_note"`
|
||
Diseases map[string]petDiseasePhrase `json:"diseases"`
|
||
}
|
||
|
||
type petSymptomQuestions struct {
|
||
Confirm string `json:"confirm"`
|
||
Short string `json:"short"`
|
||
Choices []petChoiceQuestion `json:"choices"`
|
||
}
|
||
|
||
type petChoiceQuestion struct {
|
||
Title string `json:"title"`
|
||
Question string `json:"question"`
|
||
Multiple bool `json:"multiple"`
|
||
Options []petChoiceOption `json:"options"`
|
||
}
|
||
|
||
type petChoiceOption struct {
|
||
Key string `json:"key"`
|
||
Label string `json:"label"`
|
||
}
|
||
|
||
type petDiseasePhrase struct {
|
||
Phrase string `json:"phrase"`
|
||
}
|
||
|
||
// scriptKeyGenerator produces stable ASCII keys for dimension values and
|
||
// scripts. Chinese display names live in the Name field.
|
||
type scriptKeyGenerator struct {
|
||
counters map[string]int
|
||
}
|
||
|
||
func (g *scriptKeyGenerator) key(prefix string) string {
|
||
g.counters[prefix]++
|
||
return fmt.Sprintf("%s_%03d", prefix, g.counters[prefix])
|
||
}
|
||
|
||
// buildScriptPackage builds the 问诊问药 script package from the docs files.
|
||
func buildScriptPackage(scriptFile, knowledgeFile, smallCategoryFile, smallAskScriptFile, preGuideFile, classificationFile, questionFile string) (scriptkit.PackageInput, error) {
|
||
bank, err := readQuestionBank(questionFile)
|
||
if err != nil {
|
||
return scriptkit.PackageInput{}, err
|
||
}
|
||
classification, err := readClassification(classificationFile)
|
||
if err != nil {
|
||
return scriptkit.PackageInput{}, err
|
||
}
|
||
smallToBig, err := readStringMap(smallCategoryFile)
|
||
if err != nil {
|
||
return scriptkit.PackageInput{}, err
|
||
}
|
||
smallAsk, err := readPetScriptMap(smallAskScriptFile)
|
||
if err != nil {
|
||
return scriptkit.PackageInput{}, err
|
||
}
|
||
preGuide, err := readPreGuide(preGuideFile)
|
||
if err != nil {
|
||
return scriptkit.PackageInput{}, err
|
||
}
|
||
combinations, combinationSymptoms, err := readCombinations(scriptFile)
|
||
if err != nil {
|
||
return scriptkit.PackageInput{}, err
|
||
}
|
||
rows, err := readPetKnowledge(knowledgeFile)
|
||
if err != nil {
|
||
return scriptkit.PackageInput{}, err
|
||
}
|
||
|
||
keys := &scriptKeyGenerator{counters: map[string]int{}}
|
||
input := scriptkit.PackageInput{
|
||
Name: "宠物问诊问药话术包",
|
||
StartStageKey: "opening",
|
||
Dimensions: []scriptkit.DimensionInput{},
|
||
Stages: []scriptkit.StageInput{},
|
||
Linkages: []scriptkit.LinkageInput{},
|
||
Adapters: []scriptkit.AdapterInput{},
|
||
}
|
||
|
||
// 症状大类来自小类映射、诊断分组、组合话术和 SKU 表的并集。
|
||
bigSet := map[string]bool{}
|
||
for _, big := range smallToBig {
|
||
if big != "" {
|
||
bigSet[big] = true
|
||
}
|
||
}
|
||
for _, diagnosis := range classification.Diagnosis {
|
||
if diagnosis.Type != "" {
|
||
bigSet[diagnosis.Type] = true
|
||
}
|
||
}
|
||
for big := range combinationSymptoms {
|
||
bigSet[big] = true
|
||
}
|
||
for _, row := range rows {
|
||
if row.Symptom != "" {
|
||
bigSet[row.Symptom] = true
|
||
}
|
||
}
|
||
bigNames := make([]string, 0, len(bigSet))
|
||
for name := range bigSet {
|
||
bigNames = append(bigNames, name)
|
||
}
|
||
sort.Strings(bigNames)
|
||
bigKey := map[string]string{}
|
||
bigDimension := scriptkit.DimensionInput{DimKey: "symptom", Name: "症状", SortOrder: 1, Values: []scriptkit.ValueInput{}}
|
||
for _, name := range bigNames {
|
||
key := keys.key("symptom")
|
||
bigKey[name] = key
|
||
bigDimension.Values = append(bigDimension.Values, scriptkit.ValueInput{ValueKey: key, Name: name, InitialWeight: 0, SortOrder: len(bigDimension.Values)})
|
||
}
|
||
input.Dimensions = append(input.Dimensions, bigDimension)
|
||
|
||
subSet := map[string]bool{}
|
||
for sub := range smallToBig {
|
||
if sub != "" {
|
||
subSet[sub] = true
|
||
}
|
||
}
|
||
for _, symptom := range classification.Symptoms {
|
||
for _, sub := range symptom.Subs {
|
||
if sub != "" {
|
||
subSet[sub] = true
|
||
}
|
||
}
|
||
}
|
||
subNames := make([]string, 0, len(subSet))
|
||
for name := range subSet {
|
||
subNames = append(subNames, name)
|
||
}
|
||
sort.Strings(subNames)
|
||
subKey := map[string]string{}
|
||
subDimension := scriptkit.DimensionInput{DimKey: "symptom_sub", Name: "症状小类", SortOrder: 2, Values: []scriptkit.ValueInput{}}
|
||
for _, name := range subNames {
|
||
key := keys.key("symptom_sub")
|
||
subKey[name] = key
|
||
subDimension.Values = append(subDimension.Values, scriptkit.ValueInput{ValueKey: key, Name: name, InitialWeight: 0, SortOrder: len(subDimension.Values)})
|
||
}
|
||
input.Dimensions = append(input.Dimensions, subDimension)
|
||
|
||
diseaseNames := make([]string, 0)
|
||
diseaseKey := map[string]string{}
|
||
for _, diagnosis := range classification.Diagnosis {
|
||
for _, sub := range diagnosis.Subs {
|
||
if sub.SubType == "" {
|
||
continue
|
||
}
|
||
diseaseNames = append(diseaseNames, sub.SubType)
|
||
}
|
||
}
|
||
sort.Strings(diseaseNames)
|
||
diseaseDimension := scriptkit.DimensionInput{DimKey: "disease", Name: "疾病分型", SortOrder: 3, Values: []scriptkit.ValueInput{}}
|
||
for _, name := range diseaseNames {
|
||
key := keys.key("disease")
|
||
diseaseKey[name] = key
|
||
diseaseDimension.Values = append(diseaseDimension.Values, scriptkit.ValueInput{ValueKey: key, Name: name, InitialWeight: 0, SortOrder: len(diseaseDimension.Values)})
|
||
}
|
||
input.Dimensions = append(input.Dimensions, diseaseDimension)
|
||
|
||
// 每个症状都必须配置确认题和选择题;每个疾病都必须配置口语化说法。
|
||
var missingSymptoms, missingDiseases []string
|
||
for _, name := range bigNames {
|
||
questions, ok := bank.Symptoms[name]
|
||
if !ok || strings.TrimSpace(questions.Confirm) == "" || len(questions.Choices) == 0 {
|
||
missingSymptoms = append(missingSymptoms, name)
|
||
}
|
||
}
|
||
for _, name := range diseaseNames {
|
||
if _, ok := bank.Diseases[name]; !ok {
|
||
missingDiseases = append(missingDiseases, name)
|
||
}
|
||
}
|
||
if len(missingSymptoms) > 0 {
|
||
return scriptkit.PackageInput{}, fmt.Errorf("话术库 %s 缺少症状确认题/选择题: %v", questionFile, missingSymptoms)
|
||
}
|
||
if len(missingDiseases) > 0 {
|
||
return scriptkit.PackageInput{}, fmt.Errorf("话术库 %s 缺少疾病口语化说法: %v", questionFile, missingDiseases)
|
||
}
|
||
|
||
// Linkages: 小类 -> 大类, 大类 -> 疾病. 激活阈值 5:只有已确认的症状
|
||
// (权重达到 5 以上)才会把疾病带入拟诊阶段;模糊提示(权重 3)只触发
|
||
// 症状确认题,不会直接带出疾病。
|
||
linkages := make([]scriptkit.LinkageInput, 0)
|
||
for sub, big := range smallToBig {
|
||
if subKey[sub] == "" || bigKey[big] == "" {
|
||
continue
|
||
}
|
||
linkages = append(linkages, scriptkit.LinkageInput{FromDimensionValueKey: subKey[sub], ToDimensionValueKey: bigKey[big], RelationType: "contributes_to", Contribution: 8, ActivationThreshold: 5, SortOrder: len(linkages)})
|
||
}
|
||
for _, diagnosis := range classification.Diagnosis {
|
||
for _, sub := range diagnosis.Subs {
|
||
if bigKey[diagnosis.Type] == "" || diseaseKey[sub.SubType] == "" {
|
||
continue
|
||
}
|
||
linkages = append(linkages, scriptkit.LinkageInput{FromDimensionValueKey: bigKey[diagnosis.Type], ToDimensionValueKey: diseaseKey[sub.SubType], RelationType: "contributes_to", Contribution: 5, ActivationThreshold: 5, SortOrder: len(linkages)})
|
||
}
|
||
}
|
||
sort.SliceStable(linkages, func(i, j int) bool { return linkages[i].SortOrder < linkages[j].SortOrder })
|
||
input.Linkages = linkages
|
||
|
||
// Adapters: symptom tags and order SKUs initialize symptom weights.
|
||
// SKU 只提示相关症状(权重 3,触发症状确认题),不会直接命中疾病——
|
||
// 疾病必须由已确认的症状联动带出。
|
||
adapters := make([]scriptkit.AdapterInput, 0)
|
||
for _, name := range bigNames {
|
||
adapters = append(adapters, scriptkit.AdapterInput{SourceField: "input.input_symptom_tags", MatchValue: name, TargetDimensionValueKey: bigKey[name], Weight: 8, SortOrder: len(adapters)})
|
||
}
|
||
for _, name := range subNames {
|
||
adapters = append(adapters, scriptkit.AdapterInput{SourceField: "input.input_symptom_tags", MatchValue: name, TargetDimensionValueKey: subKey[name], Weight: 8, SortOrder: len(adapters)})
|
||
}
|
||
for _, row := range rows {
|
||
if strings.TrimSpace(row.SKUCode) == "" {
|
||
continue
|
||
}
|
||
if bigKey[row.Symptom] != "" {
|
||
adapters = append(adapters, scriptkit.AdapterInput{SourceField: "derived.matched_skus", MatchValue: row.SKUCode, TargetDimensionValueKey: bigKey[row.Symptom], Weight: 3, SortOrder: len(adapters)})
|
||
}
|
||
}
|
||
sort.SliceStable(adapters, func(i, j int) bool { return adapters[i].SortOrder < adapters[j].SortOrder })
|
||
input.Adapters = adapters
|
||
|
||
// Stage 1: 开场. 开场只做问候和宠物档案补充,不显示任何维度(症状只在症状节点显示)。
|
||
opening := scriptkit.StageInput{
|
||
StageKey: "opening", Name: "开场", Purpose: "确认顾客与宠物信息,并补充宠物档案", PrimaryDimensionKey: "", SortOrder: 1, Scripts: []scriptkit.ScriptInput{
|
||
{ScriptKey: "opening_greeting", Name: "开场话术", ScriptType: "template", Content: bank.OpeningGreeting, SortOrder: 10},
|
||
},
|
||
}
|
||
input.Stages = append(input.Stages, opening)
|
||
|
||
// Stage 2: 症状确认。候选症状超过 3 个时先做一道多选筛选题,
|
||
// 筛完再对选中的症状按由浅入深的顺序做选择题。
|
||
symptomStage := scriptkit.StageInput{StageKey: "symptom", Name: "症状确认", Purpose: "确认宠物当前的主要症状", PrimaryDimensionKey: "symptom", SortOrder: 2, Scripts: []scriptkit.ScriptInput{}}
|
||
for index, guide := range preGuide {
|
||
symptomStage.Scripts = append(symptomStage.Scripts, scriptkit.ScriptInput{ScriptKey: keys.key("symptom_guide"), Name: "引导话术" + guide.Title, ScriptType: "fallback", Content: guide.Template, SortOrder: 100 + index})
|
||
}
|
||
screenOptions := make([]scriptkit.OptionInput, 0, len(bigNames))
|
||
for _, name := range bigNames {
|
||
short := bank.Symptoms[name].Short
|
||
if short == "" {
|
||
short = name
|
||
}
|
||
screenOptions = append(screenOptions, scriptkit.OptionInput{OptionKey: bigKey[name], Label: short, TargetDimensionValueKey: bigKey[name], Effect: "set", EffectValue: 10, SortOrder: len(screenOptions) + 1})
|
||
}
|
||
symptomStage.Scripts = append(symptomStage.Scripts, scriptkit.ScriptInput{
|
||
ScriptKey: keys.key("symptom_screen"), Name: "症状筛选", ScriptType: "screen",
|
||
Content: bank.SymptomScreeningTemplate, Multiple: true,
|
||
SortOrder: 50, Options: screenOptions,
|
||
})
|
||
for _, name := range bigNames {
|
||
questions := bank.Symptoms[name]
|
||
confirm := scriptkit.ScriptInput{
|
||
ScriptKey: keys.key("symptom_confirm"), Name: "确认" + name + "症状", ScriptType: "confirm",
|
||
Content: questions.Confirm,
|
||
DimensionValueKey: bigKey[name], ConfirmThreshold: 3, ShowThreshold: 5, SortOrder: 1000,
|
||
Options: []scriptkit.OptionInput{
|
||
{OptionKey: "yes", Label: bank.SymptomConfirmOptions.Yes, TargetDimensionValueKey: bigKey[name], Effect: "set", EffectValue: 8, SortOrder: 1},
|
||
{OptionKey: "no", Label: bank.SymptomConfirmOptions.No, TargetDimensionValueKey: bigKey[name], Effect: "zero", EffectValue: 0, SortOrder: 2},
|
||
},
|
||
}
|
||
symptomStage.Scripts = append(symptomStage.Scripts, confirm)
|
||
for choiceIndex, choice := range questions.Choices {
|
||
options := make([]scriptkit.OptionInput, 0, len(choice.Options))
|
||
for optionIndex, option := range choice.Options {
|
||
options = append(options, scriptkit.OptionInput{OptionKey: option.Key, Label: option.Label, Effect: "set", EffectValue: 0, SortOrder: optionIndex + 1})
|
||
}
|
||
scriptKey := keys.key("symptom_choice")
|
||
symptomStage.Scripts = append(symptomStage.Scripts, scriptkit.ScriptInput{
|
||
ScriptKey: scriptKey, Name: name + choice.Title, ScriptType: "choice",
|
||
Content: choice.Question, DimensionValueKey: bigKey[name],
|
||
ConfirmThreshold: 3, ShowThreshold: 5, CollectFieldKey: scriptKey,
|
||
Multiple: choice.Multiple,
|
||
SortOrder: 1100 + choiceIndex, Options: options,
|
||
})
|
||
}
|
||
}
|
||
for _, name := range subNames {
|
||
for index, ask := range smallAsk[name] {
|
||
symptomStage.Scripts = append(symptomStage.Scripts, scriptkit.ScriptInput{ScriptKey: keys.key("symptom_sub_ask"), Name: name + ask.Title, ScriptType: "info", Content: ask.Template, DimensionValueKey: subKey[name], ConfirmThreshold: 3, ShowThreshold: 5, SortOrder: 5000 + index})
|
||
}
|
||
}
|
||
input.Stages = append(input.Stages, symptomStage)
|
||
|
||
// Stage 3: 拟诊确认. 候选疾病(由已确认症状带出,权重5)先显示确认题;
|
||
// 客户确认后(权重10)才显示该疾病的确认话术,可同时确认多个疾病。
|
||
diagnosisStage := scriptkit.StageInput{StageKey: "diagnosis", Name: "拟诊确认", Purpose: "结合症状确认可能的疾病方向(可确认多个),确认后再进入推荐", PrimaryDimensionKey: "disease", SortOrder: 3, Scripts: []scriptkit.ScriptInput{
|
||
{ScriptKey: "diagnosis_fallback", Name: "拟诊兜底话术", ScriptType: "fallback", Content: bank.DiagnosisFallback, SortOrder: 100},
|
||
}}
|
||
for _, name := range diseaseNames {
|
||
phrase := bank.Diseases[name].Phrase
|
||
confirm := scriptkit.ScriptInput{
|
||
ScriptKey: keys.key("disease_confirm"), Name: "确认" + name + "方向", ScriptType: "confirm",
|
||
Content: strings.ReplaceAll(bank.DiseaseConfirmTemplate, "{{disease_phrase}}", phrase),
|
||
DimensionValueKey: diseaseKey[name], ConfirmThreshold: 5, ShowThreshold: 10, SortOrder: 1000,
|
||
Options: []scriptkit.OptionInput{
|
||
{OptionKey: "yes", Label: bank.DiseaseConfirmOptions.Yes, TargetDimensionValueKey: diseaseKey[name], Effect: "set", EffectValue: 10, SortOrder: 1},
|
||
{OptionKey: "no", Label: bank.DiseaseConfirmOptions.No, TargetDimensionValueKey: diseaseKey[name], Effect: "zero", EffectValue: 0, SortOrder: 2},
|
||
},
|
||
}
|
||
diagnosisStage.Scripts = append(diagnosisStage.Scripts, confirm)
|
||
diagnosisStage.Scripts = append(diagnosisStage.Scripts, scriptkit.ScriptInput{
|
||
ScriptKey: keys.key("disease_copy"), Name: name + "疾病确认话术", ScriptType: "message",
|
||
Content: strings.ReplaceAll(bank.DiseaseConfirmedCopyTemplate, "{{disease_phrase}}", phrase),
|
||
DimensionValueKey: diseaseKey[name], ConfirmThreshold: 5, ShowThreshold: 10, SortOrder: 1100,
|
||
})
|
||
diagnosisStage.Scripts = append(diagnosisStage.Scripts, scriptkit.ScriptInput{
|
||
ScriptKey: keys.key("disease_copy"), Name: name + "方案引入话术", ScriptType: "message",
|
||
Content: bank.DiseasePlanIntro,
|
||
DimensionValueKey: diseaseKey[name], ConfirmThreshold: 5, ShowThreshold: 10, SortOrder: 1101,
|
||
})
|
||
}
|
||
input.Stages = append(input.Stages, diagnosisStage)
|
||
|
||
// Stage 4: 药品推荐与注意事项. 只有客户确认过的疾病(权重10)才会推荐。
|
||
recommendStage := scriptkit.StageInput{StageKey: "recommend", Name: "药品推荐与注意事项", Purpose: "按已确认的疾病方向推荐方案、商品和注意事项", PrimaryDimensionKey: "disease", SortOrder: 4, Scripts: []scriptkit.ScriptInput{
|
||
{ScriptKey: "recommend_fallback", Name: "推荐兜底话术", ScriptType: "fallback", Content: bank.RecommendFallback, SortOrder: 100},
|
||
}}
|
||
for _, name := range diseaseNames {
|
||
order := 1000
|
||
combos := combinations[name]
|
||
sort.SliceStable(combos, func(i, j int) bool { return combos[i].Weight > combos[j].Weight })
|
||
for _, combination := range combos {
|
||
products := make([]scriptkit.ProductInput, 0, len(combination.Products))
|
||
for _, product := range combination.Products {
|
||
if strings.TrimSpace(product.SKUCode) == "" {
|
||
continue
|
||
}
|
||
products = append(products, scriptkit.ProductInput{SKUCode: product.SKUCode, ProductName: product.ProductName})
|
||
}
|
||
recommendStage.Scripts = append(recommendStage.Scripts, scriptkit.ScriptInput{
|
||
ScriptKey: keys.key("recommend_combination"), Name: name + combination.Name, ScriptType: "message",
|
||
Content: strings.ReplaceAll(combination.Script, "{{purchased_products}}", "{{input.product_names}}"),
|
||
DimensionValueKey: diseaseKey[name], ConfirmThreshold: 5, ShowThreshold: 10,
|
||
Products: products, SortOrder: order,
|
||
})
|
||
order++
|
||
}
|
||
recommendStage.Scripts = append(recommendStage.Scripts, scriptkit.ScriptInput{
|
||
ScriptKey: keys.key("recommend_note"), Name: name + "注意事项", ScriptType: "message",
|
||
Content: bank.DiseaseCareNote,
|
||
DimensionValueKey: diseaseKey[name], ConfirmThreshold: 5, ShowThreshold: 10, SortOrder: order,
|
||
})
|
||
}
|
||
input.Stages = append(input.Stages, recommendStage)
|
||
|
||
// Verify generated keys are valid before returning.
|
||
if err := scriptkit.ValidatePackageInput(input); err != nil {
|
||
return scriptkit.PackageInput{}, fmt.Errorf("generated package is invalid: %w", err)
|
||
}
|
||
return input, nil
|
||
}
|
||
|
||
func readQuestionBank(filename string) (petQuestionBank, error) {
|
||
raw, err := os.ReadFile(filename)
|
||
if err != nil {
|
||
return petQuestionBank{}, fmt.Errorf("read pet question bank %s: %w", filename, err)
|
||
}
|
||
var bank petQuestionBank
|
||
if err := json.Unmarshal(raw, &bank); err != nil {
|
||
return petQuestionBank{}, fmt.Errorf("parse pet question bank %s: %w", filename, err)
|
||
}
|
||
if bank.SymptomConfirmOptions.Yes == "" {
|
||
bank.SymptomConfirmOptions.Yes = "有"
|
||
}
|
||
if bank.SymptomConfirmOptions.No == "" {
|
||
bank.SymptomConfirmOptions.No = "没有"
|
||
}
|
||
if bank.DiseaseConfirmOptions.Yes == "" {
|
||
bank.DiseaseConfirmOptions.Yes = "对,按这个判断"
|
||
}
|
||
if bank.DiseaseConfirmOptions.No == "" {
|
||
bank.DiseaseConfirmOptions.No = "感觉不太像"
|
||
}
|
||
if bank.SymptomScreeningTemplate == "" {
|
||
return petQuestionBank{}, fmt.Errorf("pet question bank %s is missing symptom_screening_template", filename)
|
||
}
|
||
if bank.DiseaseConfirmTemplate == "" || bank.DiseaseConfirmedCopyTemplate == "" {
|
||
return petQuestionBank{}, fmt.Errorf("pet question bank %s is missing disease templates", filename)
|
||
}
|
||
return bank, nil
|
||
}
|
||
|
||
func readClassification(filename string) (petClassification, error) {
|
||
raw, err := os.ReadFile(filename)
|
||
if err != nil {
|
||
return petClassification{}, fmt.Errorf("read pet classification %s: %w", filename, err)
|
||
}
|
||
var wrapper struct {
|
||
ConsultPayload string `json:"consultPayload"`
|
||
}
|
||
if err := json.Unmarshal(raw, &wrapper); err != nil {
|
||
return petClassification{}, fmt.Errorf("parse pet classification wrapper %s: %w", filename, err)
|
||
}
|
||
var result petClassification
|
||
if err := json.Unmarshal([]byte(wrapper.ConsultPayload), &result); err != nil {
|
||
return petClassification{}, fmt.Errorf("parse pet classification %s: %w", filename, err)
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func readStringMap(filename string) (map[string]string, error) {
|
||
raw, err := os.ReadFile(filename)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("read pet string map %s: %w", filename, err)
|
||
}
|
||
var result map[string]string
|
||
if err := json.Unmarshal(raw, &result); err != nil {
|
||
return nil, fmt.Errorf("parse pet string map %s: %w", filename, err)
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func readPetScriptMap(filename string) (map[string][]petScriptEntry, error) {
|
||
raw, err := os.ReadFile(filename)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("read pet script map %s: %w", filename, err)
|
||
}
|
||
var source map[string][]petScriptEntry
|
||
if err := json.Unmarshal(raw, &source); err != nil {
|
||
return nil, fmt.Errorf("parse pet script map %s: %w", filename, err)
|
||
}
|
||
return source, nil
|
||
}
|
||
|
||
func readPreGuide(filename string) ([]petScriptEntry, error) {
|
||
raw, err := os.ReadFile(filename)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("read pet pre-symptom guide %s: %w", filename, err)
|
||
}
|
||
var source struct {
|
||
PreSymptomScripts []petScriptEntry `json:"pre_symptom_scripts"`
|
||
}
|
||
if err := json.Unmarshal(raw, &source); err != nil {
|
||
return nil, fmt.Errorf("parse pet pre-symptom guide %s: %w", filename, err)
|
||
}
|
||
return source.PreSymptomScripts, nil
|
||
}
|
||
|
||
// readCombinations loads 疾病 -> 组合方案话术 and the symptom keys of the source.
|
||
func readCombinations(filename string) (map[string][]petCombination, map[string]bool, error) {
|
||
raw, err := os.ReadFile(filename)
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("read pet script combinations %s: %w", filename, err)
|
||
}
|
||
var source map[string]map[string]petScriptDisease
|
||
if err := json.Unmarshal(raw, &source); err != nil {
|
||
return nil, nil, fmt.Errorf("parse pet script combinations %s: %w", filename, err)
|
||
}
|
||
result := map[string][]petCombination{}
|
||
symptoms := map[string]bool{}
|
||
for symptom, diseases := range source {
|
||
symptoms[symptom] = true
|
||
for disease, definition := range diseases {
|
||
result[disease] = append(result[disease], definition.Combinations...)
|
||
}
|
||
}
|
||
return result, symptoms, nil
|
||
}
|
||
|
||
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["疾病"]),
|
||
SKUCode: excelValue(values, columns["SKU_CODE"]),
|
||
ProductName: excelValue(values, columns["商品标题"]),
|
||
}
|
||
if row.Symptom == "" && row.Disease == "" {
|
||
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])
|
||
}
|