Files
iqudo-top1/internal/scriptkit/handler.go
Eric 1549169735@qq.com da3f16e6db update
2026-09-13 20:08:13 +08:00

323 lines
14 KiB
Go

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
}