This commit is contained in:
Eric 1549169735@qq.com
2026-09-13 20:08:13 +08:00
parent 7cc06976ab
commit da3f16e6db
65 changed files with 3855 additions and 2153 deletions

View File

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