package scriptkit import ( "encoding/json" "net/http" "strconv" "git.iwork-ai.com/xdc/iqudo-top1/internal/access" "git.iwork-ai.com/xdc/iqudo-top1/internal/audit" "git.iwork-ai.com/xdc/iqudo-top1/internal/auth" "git.iwork-ai.com/xdc/iqudo-top1/internal/model" "git.iwork-ai.com/xdc/iqudo-top1/internal/response" "github.com/gin-gonic/gin" "gorm.io/datatypes" "gorm.io/gorm" ) // Handler exposes the script package admin API. type Handler struct { db *gorm.DB } func NewHandler(db *gorm.DB) *Handler { return &Handler{db: db} } // PackageInput is the replaceable document of a script package. type PackageInput struct { Name string `json:"name"` StartStageKey string `json:"start_stage_key"` Dimensions []DimensionInput `json:"dimensions"` Stages []StageInput `json:"stages"` Linkages []LinkageInput `json:"linkages"` Adapters []AdapterInput `json:"adapters"` } // DimensionInput is one dimension with its values. type DimensionInput struct { DimKey string `json:"dim_key"` Name string `json:"name"` SortOrder int `json:"sort_order"` Values []ValueInput `json:"values"` } // ValueInput is one dimension value. type ValueInput struct { ValueKey string `json:"value_key"` Name string `json:"name"` InitialWeight int `json:"initial_weight"` SortOrder int `json:"sort_order"` } // StageInput is one package stage with its scripts. type StageInput struct { StageKey string `json:"stage_key"` Name string `json:"name"` Purpose string `json:"purpose"` PrimaryDimensionKey string `json:"primary_dimension_key"` SortOrder int `json:"sort_order"` Scripts []ScriptInput `json:"scripts"` } // ScriptInput is one stage script. type ScriptInput struct { ScriptKey string `json:"script_key"` Name string `json:"name"` ScriptType string `json:"script_type"` Content string `json:"content"` DimensionValueKey string `json:"dimension_value_key"` ShowThreshold int `json:"show_threshold"` ConfirmThreshold int `json:"confirm_threshold"` CollectFieldKey string `json:"collect_field_key"` Required bool `json:"required"` Multiple bool `json:"multiple"` Products []ProductInput `json:"products"` SortOrder int `json:"sort_order"` Options []OptionInput `json:"options"` } // ProductInput is one static product attached to a script. type ProductInput struct { SKUCode string `json:"sku_code"` ProductName string `json:"product_name"` } // OptionInput is one answer option of a script. type OptionInput struct { OptionKey string `json:"option_key"` Label string `json:"label"` TargetDimensionValueKey string `json:"target_dimension_value_key"` Effect string `json:"effect"` EffectValue int `json:"effect_value"` SortOrder int `json:"sort_order"` } // LinkageInput is one dimension linkage. type LinkageInput struct { FromDimensionValueKey string `json:"from_dimension_value_key"` ToDimensionValueKey string `json:"to_dimension_value_key"` RelationType string `json:"relation_type"` Contribution int `json:"contribution"` ActivationThreshold int `json:"activation_threshold"` SortOrder int `json:"sort_order"` } // AdapterInput is one entry weight adapter. type AdapterInput struct { SourceField string `json:"source_field"` MatchValue string `json:"match_value"` TargetDimensionValueKey string `json:"target_dimension_value_key"` Weight int `json:"weight"` SortOrder int `json:"sort_order"` } func scenarioPackageID(c *gin.Context) (uint64, bool) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil || id == 0 { response.Error(c, http.StatusBadRequest, "INVALID_ID", "场景 ID 不正确") return 0, false } return id, true } // Get returns the script package of a scenario as a nested JSON document. func (h *Handler) Get(c *gin.Context) { p, _ := auth.PrincipalFromContext(c) scenarioID, ok := scenarioPackageID(c) if !ok || !access.CanViewScenario(h.db, p, scenarioID) { if ok { response.Error(c, http.StatusNotFound, "NOT_FOUND", "场景不存在") } return } pkg, err := LoadPackageByScenario(h.db, p.TenantID, scenarioID) if err != nil { response.Error(c, http.StatusNotFound, "NOT_FOUND", "话术包不存在") return } response.OK(c, PackageDocument(pkg)) } // Replace replaces the whole script package of a scenario in one transaction. func (h *Handler) Replace(c *gin.Context) { p, _ := auth.PrincipalFromContext(c) scenarioID, ok := scenarioPackageID(c) if !ok || !access.CanEditScenario(h.db, p, scenarioID) { if ok { response.Error(c, http.StatusNotFound, "NOT_FOUND", "场景不存在或不可编辑") } return } var input PackageInput if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "话术包 JSON 格式不正确") return } if err := ValidatePackageInput(input); err != nil { response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", err.Error()) return } if err := SavePackage(h.db, p.TenantID, p.UserID, scenarioID, input); err != nil { response.Error(c, http.StatusInternalServerError, "SAVE_FAILED", "保存话术包失败") return } h.Get(c) } // SavePackage replaces the script package of a scenario in one transaction. func SavePackage(db *gorm.DB, tenantID, userID, scenarioID uint64, input PackageInput) error { if err := ValidatePackageInput(input); err != nil { return err } var old model.ScriptPackage hadOld := true if err := db.Where("scenario_id = ? AND tenant_id = ?", scenarioID, tenantID).First(&old).Error; err != nil { hadOld = false } principal := auth.Principal{TenantID: tenantID, UserID: userID} return db.Transaction(func(tx *gorm.DB) error { if hadOld { for _, target := range []interface{}{ &model.ScriptOption{}, &model.StageScript{}, &model.DimensionLinkage{}, &model.PackageAdapter{}, &model.PackageStage{}, &model.DimensionValue{}, &model.PackageDimension{}, } { if err := tx.Where("tenant_id = ? AND package_id = ?", tenantID, old.ID).Delete(target).Error; err != nil { return err } } } pkg := model.ScriptPackage{TenantID: tenantID, ScenarioID: scenarioID, Name: input.Name, Status: "active", StartStageKey: input.StartStageKey, CreatedBy: userID} if hadOld { pkg.ID = old.ID pkg.CreatedAt = old.CreatedAt } if err := tx.Save(&pkg).Error; err != nil { return err } dimensionIDs := map[string]uint64{} for _, dimension := range input.Dimensions { row := model.PackageDimension{TenantID: tenantID, PackageID: pkg.ID, DimKey: dimension.DimKey, Name: dimension.Name, SortOrder: dimension.SortOrder} if err := tx.Create(&row).Error; err != nil { return err } dimensionIDs[dimension.DimKey] = row.ID } valueIDs := map[string]uint64{} for _, dimension := range input.Dimensions { for _, value := range dimension.Values { row := model.DimensionValue{TenantID: tenantID, PackageID: pkg.ID, DimensionID: dimensionIDs[dimension.DimKey], ValueKey: value.ValueKey, Name: value.Name, InitialWeight: value.InitialWeight, SortOrder: value.SortOrder} if err := tx.Create(&row).Error; err != nil { return err } valueIDs[value.ValueKey] = row.ID } } for _, stage := range input.Stages { row := model.PackageStage{TenantID: tenantID, PackageID: pkg.ID, StageKey: stage.StageKey, Name: stage.Name, Purpose: stage.Purpose, PrimaryDimensionID: dimensionIDs[stage.PrimaryDimensionKey], SortOrder: stage.SortOrder} if err := tx.Create(&row).Error; err != nil { return err } for _, script := range stage.Scripts { var dimensionValueID *uint64 if script.DimensionValueKey != "" { id := valueIDs[script.DimensionValueKey] dimensionValueID = &id } products, _ := json.Marshal(script.Products) if script.Products == nil { products = []byte(`[]`) } scriptRow := model.StageScript{TenantID: tenantID, PackageID: pkg.ID, StageID: row.ID, ScriptKey: script.ScriptKey, Name: script.Name, ScriptType: script.ScriptType, Content: script.Content, DimensionValueID: dimensionValueID, ShowThreshold: script.ShowThreshold, ConfirmThreshold: script.ConfirmThreshold, CollectFieldKey: script.CollectFieldKey, Required: script.Required, Multiple: script.Multiple, Products: datatypes.JSON(products), SortOrder: script.SortOrder} if err := tx.Create(&scriptRow).Error; err != nil { return err } for _, option := range script.Options { var targetID *uint64 if option.TargetDimensionValueKey != "" { id := valueIDs[option.TargetDimensionValueKey] targetID = &id } optionRow := model.ScriptOption{TenantID: tenantID, PackageID: pkg.ID, ScriptID: scriptRow.ID, OptionKey: option.OptionKey, Label: option.Label, TargetDimensionValueID: targetID, Effect: option.Effect, EffectValue: option.EffectValue, SortOrder: option.SortOrder} if err := tx.Create(&optionRow).Error; err != nil { return err } } } } for _, linkage := range input.Linkages { row := model.DimensionLinkage{TenantID: tenantID, PackageID: pkg.ID, FromDimensionValueID: valueIDs[linkage.FromDimensionValueKey], ToDimensionValueID: valueIDs[linkage.ToDimensionValueKey], RelationType: linkage.RelationType, Contribution: linkage.Contribution, ActivationThreshold: linkage.ActivationThreshold, Condition: datatypes.JSON([]byte(`{}`)), SortOrder: linkage.SortOrder} if err := tx.Create(&row).Error; err != nil { return err } } for _, adapter := range input.Adapters { row := model.PackageAdapter{TenantID: tenantID, PackageID: pkg.ID, SourceField: adapter.SourceField, MatchValue: adapter.MatchValue, TargetDimensionValueID: valueIDs[adapter.TargetDimensionValueKey], Weight: adapter.Weight, SortOrder: adapter.SortOrder} if err := tx.Create(&row).Error; err != nil { return err } } return audit.RecordTx(tx, principal, "replace", "script_package", pkg.ID, gin.H{"scenario_id": scenarioID}) }) } // PackageDocument renders a loaded package as a nested JSON document. func PackageDocument(pkg *Package) gin.H { valuesByDimension := map[uint64][]ValueInput{} valueKeys := map[uint64]string{} dimensionKeys := map[uint64]string{} for _, value := range pkg.Values { valueKeys[value.ID] = value.ValueKey valuesByDimension[value.DimensionID] = append(valuesByDimension[value.DimensionID], ValueInput{ValueKey: value.ValueKey, Name: value.Name, InitialWeight: value.InitialWeight, SortOrder: value.SortOrder}) } for _, dimension := range pkg.Dimensions { dimensionKeys[dimension.ID] = dimension.DimKey } dimensions := make([]DimensionInput, 0, len(pkg.Dimensions)) for _, dimension := range pkg.Dimensions { dimensions = append(dimensions, DimensionInput{DimKey: dimension.DimKey, Name: dimension.Name, SortOrder: dimension.SortOrder, Values: valuesByDimension[dimension.ID]}) } stages := make([]StageInput, 0, len(pkg.Stages)) for _, stage := range pkg.Stages { item := StageInput{StageKey: stage.StageKey, Name: stage.Name, Purpose: stage.Purpose, PrimaryDimensionKey: dimensionKeys[stage.PrimaryDimensionID], SortOrder: stage.SortOrder, Scripts: []ScriptInput{}} for _, script := range pkg.Scripts { if script.StageID != stage.ID { continue } scriptItem := ScriptInput{ScriptKey: script.ScriptKey, Name: script.Name, ScriptType: script.ScriptType, Content: script.Content, DimensionValueKey: valueKeys[derefID(script.DimensionValueID)], ShowThreshold: script.ShowThreshold, ConfirmThreshold: script.ConfirmThreshold, CollectFieldKey: script.CollectFieldKey, Required: script.Required, Multiple: script.Multiple, Products: []ProductInput{}, SortOrder: script.SortOrder, Options: []OptionInput{}} var products []ProductInput if err := json.Unmarshal(script.Products, &products); err == nil && products != nil { scriptItem.Products = products } for _, option := range pkg.Options { if option.ScriptID != script.ID { continue } scriptItem.Options = append(scriptItem.Options, OptionInput{OptionKey: option.OptionKey, Label: option.Label, TargetDimensionValueKey: valueKeys[derefID(option.TargetDimensionValueID)], Effect: option.Effect, EffectValue: option.EffectValue, SortOrder: option.SortOrder}) } item.Scripts = append(item.Scripts, scriptItem) } stages = append(stages, item) } linkages := make([]LinkageInput, 0, len(pkg.Linkages)) for _, linkage := range pkg.Linkages { linkages = append(linkages, LinkageInput{FromDimensionValueKey: valueKeys[linkage.FromDimensionValueID], ToDimensionValueKey: valueKeys[linkage.ToDimensionValueID], RelationType: linkage.RelationType, Contribution: linkage.Contribution, ActivationThreshold: linkage.ActivationThreshold, SortOrder: linkage.SortOrder}) } adapters := make([]AdapterInput, 0, len(pkg.Adapters)) for _, adapter := range pkg.Adapters { adapters = append(adapters, AdapterInput{SourceField: adapter.SourceField, MatchValue: adapter.MatchValue, TargetDimensionValueKey: valueKeys[adapter.TargetDimensionValueID], Weight: adapter.Weight, SortOrder: adapter.SortOrder}) } return gin.H{ "id": pkg.Package.ID, "name": pkg.Package.Name, "status": pkg.Package.Status, "start_stage_key": pkg.Package.StartStageKey, "dimensions": dimensions, "stages": stages, "linkages": linkages, "adapters": adapters, } } func derefID(value *uint64) uint64 { if value == nil { return 0 } return *value }