Files
iqudo-top1/scripts/seed-pet-doctor/package_builder.go
Eric 1549169735@qq.com da3f16e6db update
2026-09-13 20:08:13 +08:00

603 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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])
}