323 lines
14 KiB
Go
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
|
|
}
|