300 lines
11 KiB
Go
300 lines
11 KiB
Go
package sop
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"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"
|
|
"git.iwork-ai.com/xdc/iqudo-top1/internal/scriptkit"
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/datatypes"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type Handler struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewHandler(db *gorm.DB) *Handler {
|
|
return &Handler{db: db}
|
|
}
|
|
|
|
type createInput struct {
|
|
Name string `json:"name" binding:"required,max=128"`
|
|
Description string `json:"description" binding:"max=2000"`
|
|
}
|
|
|
|
type graphInput struct {
|
|
StartNodeKey string `json:"start_node_key" binding:"required,max=64"`
|
|
Nodes []nodeInput `json:"nodes" binding:"required,min=1"`
|
|
Edges []edgeInput `json:"edges"`
|
|
}
|
|
|
|
type nodeInput struct {
|
|
NodeKey string `json:"node_key" binding:"required,max=64"`
|
|
Type string `json:"type" binding:"required,max=32"`
|
|
Title string `json:"title" binding:"required,max=128"`
|
|
Content string `json:"content" binding:"max=5000"`
|
|
Config json.RawMessage `json:"config"`
|
|
PositionX int `json:"position_x"`
|
|
PositionY int `json:"position_y"`
|
|
}
|
|
|
|
type edgeInput struct {
|
|
SourceNodeKey string `json:"source_node_key" binding:"required,max=64"`
|
|
TargetNodeKey string `json:"target_node_key" binding:"required,max=64"`
|
|
Condition json.RawMessage `json:"condition"`
|
|
Priority int `json:"priority"`
|
|
}
|
|
|
|
type editState struct {
|
|
StartNodeKey string `json:"start_node_key"`
|
|
}
|
|
|
|
func (h *Handler) List(c *gin.Context) {
|
|
p, _ := auth.PrincipalFromContext(c)
|
|
items := make([]model.SOP, 0)
|
|
query := h.db.Table("sops s").Select("s.*").Joins("JOIN scenarios sc ON sc.id = s.scenario_id")
|
|
query = access.ScopeScenarios(query, p, "sc").Where("s.tenant_id = ? AND s.status <> ?", p.TenantID, "archived")
|
|
scenarioID := c.Query("scenario_id")
|
|
if scenarioID == "" {
|
|
scenarioID = c.Param("id")
|
|
}
|
|
if scenarioID != "" {
|
|
query = query.Where("s.scenario_id = ?", scenarioID)
|
|
}
|
|
if err := query.Order("s.updated_at DESC").Scan(&items).Error; err != nil {
|
|
response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询 SOP 失败")
|
|
return
|
|
}
|
|
response.OK(c, gin.H{"items": items, "total": len(items)})
|
|
}
|
|
|
|
func (h *Handler) Create(c *gin.Context) {
|
|
p, _ := auth.PrincipalFromContext(c)
|
|
scenarioID, ok := parseID(c, "id")
|
|
if !ok {
|
|
return
|
|
}
|
|
if !access.CanEditScenario(h.db, p, scenarioID) {
|
|
response.Error(c, http.StatusNotFound, "NOT_FOUND", "场景不存在或不可编辑")
|
|
return
|
|
}
|
|
var scenario model.Scenario
|
|
if err := h.db.Where("id = ? AND tenant_id = ?", scenarioID, p.TenantID).First(&scenario).Error; err != nil {
|
|
response.Error(c, http.StatusNotFound, "NOT_FOUND", "场景不存在")
|
|
return
|
|
}
|
|
var input createInput
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "SOP 信息不完整")
|
|
return
|
|
}
|
|
var item model.SOP
|
|
err := h.db.Transaction(func(tx *gorm.DB) error {
|
|
item = model.SOP{TenantID: p.TenantID, ScenarioID: scenarioID, Name: input.Name, Description: input.Description, Status: "published", CreatedBy: p.UserID, StartNodeKey: "start"}
|
|
if err := tx.Create(&item).Error; err != nil {
|
|
return err
|
|
}
|
|
nodes := []model.SOPNode{
|
|
{TenantID: p.TenantID, SOPID: item.ID, NodeKey: "start", Type: "start", Title: "开始", Content: "", Config: datatypes.JSON([]byte(`{}`)), PositionX: 0, PositionY: 0},
|
|
{TenantID: p.TenantID, SOPID: item.ID, NodeKey: "opening", Type: "message", Title: "开场", Content: "您好,我先了解一下具体情况。", Config: datatypes.JSON([]byte(`{}`)), PositionX: 0, PositionY: 120},
|
|
{TenantID: p.TenantID, SOPID: item.ID, NodeKey: "finish", Type: "finish", Title: "结束", Content: "本次沟通已完成。", Config: datatypes.JSON([]byte(`{}`)), PositionX: 0, PositionY: 240},
|
|
}
|
|
if err := tx.Create(&nodes).Error; err != nil {
|
|
return err
|
|
}
|
|
edges := []model.SOPEdge{
|
|
{TenantID: p.TenantID, SOPID: item.ID, SourceNodeKey: "start", TargetNodeKey: "opening", Condition: datatypes.JSON([]byte(`{}`)), Priority: 0},
|
|
{TenantID: p.TenantID, SOPID: item.ID, SourceNodeKey: "opening", TargetNodeKey: "finish", Condition: datatypes.JSON([]byte(`{}`)), Priority: 0},
|
|
}
|
|
if err := tx.Create(&edges).Error; err != nil {
|
|
return err
|
|
}
|
|
return tx.Model(&model.Scenario{}).Where("id = ? AND tenant_id = ?", scenarioID, p.TenantID).Update("status", "active").Error
|
|
})
|
|
if err != nil {
|
|
response.Error(c, http.StatusInternalServerError, "CREATE_FAILED", "创建 SOP 失败")
|
|
return
|
|
}
|
|
_ = audit.Record(h.db, p, "create", "sop", item.ID, input)
|
|
response.Created(c, gin.H{"sop": item, "edit": editState{StartNodeKey: item.StartNodeKey}})
|
|
}
|
|
|
|
func (h *Handler) Get(c *gin.Context) {
|
|
p, _ := auth.PrincipalFromContext(c)
|
|
id, ok := parseID(c, "id")
|
|
if !ok {
|
|
return
|
|
}
|
|
if !access.CanViewSOP(h.db, p, id) {
|
|
response.Error(c, http.StatusNotFound, "NOT_FOUND", "SOP 不存在")
|
|
return
|
|
}
|
|
var item model.SOP
|
|
var nodes []model.SOPNode
|
|
var edges []model.SOPEdge
|
|
item, nodes, edges, err := h.load(id, p.TenantID)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
response.Error(c, http.StatusNotFound, "NOT_FOUND", "SOP 不存在")
|
|
} else {
|
|
response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询 SOP 失败")
|
|
}
|
|
return
|
|
}
|
|
response.OK(c, gin.H{"sop": item, "edit": editState{StartNodeKey: item.StartNodeKey}, "nodes": nodes, "edges": edges})
|
|
}
|
|
|
|
func (h *Handler) SaveGraph(c *gin.Context) {
|
|
p, _ := auth.PrincipalFromContext(c)
|
|
id, ok := parseID(c, "id")
|
|
if !ok {
|
|
return
|
|
}
|
|
if !access.CanEditSOP(h.db, p, id) {
|
|
response.Error(c, http.StatusNotFound, "NOT_FOUND", "SOP 不存在或不可编辑")
|
|
return
|
|
}
|
|
var input graphInput
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "流程配置不完整")
|
|
return
|
|
}
|
|
item, _, _, err := h.load(id, p.TenantID)
|
|
if err != nil {
|
|
response.Error(c, http.StatusNotFound, "NOT_FOUND", "SOP 不存在")
|
|
return
|
|
}
|
|
nodes, edges := toModels(p.TenantID, id, input)
|
|
problems, validationErr := h.validateForPublish(item, input.StartNodeKey, nodes, edges, p.TenantID)
|
|
if validationErr != nil {
|
|
response.Error(c, http.StatusInternalServerError, "VALIDATION_FAILED", "校验流程失败")
|
|
return
|
|
}
|
|
if len(problems) > 0 {
|
|
response.Error(c, http.StatusUnprocessableEntity, "INVALID_GRAPH", problems[0])
|
|
return
|
|
}
|
|
err = h.db.Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Where("sop_id = ? AND tenant_id = ?", id, p.TenantID).Delete(&model.SOPEdge{}).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Where("sop_id = ? AND tenant_id = ?", id, p.TenantID).Delete(&model.SOPNode{}).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Create(&nodes).Error; err != nil {
|
|
return err
|
|
}
|
|
if len(edges) > 0 {
|
|
if err := tx.Create(&edges).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := tx.Model(&item).Updates(map[string]interface{}{"start_node_key": input.StartNodeKey, "status": "published"}).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Model(&item).Update("status", "published").Error; err != nil {
|
|
return err
|
|
}
|
|
return tx.Model(&model.Scenario{}).Where("id = ? AND tenant_id = ?", item.ScenarioID, p.TenantID).Update("status", "active").Error
|
|
})
|
|
if err != nil {
|
|
response.Error(c, http.StatusInternalServerError, "SAVE_FAILED", "保存流程失败")
|
|
return
|
|
}
|
|
_ = audit.Record(h.db, p, "save_graph", "sop", id, nil)
|
|
h.Get(c)
|
|
}
|
|
|
|
func (h *Handler) Validate(c *gin.Context) {
|
|
p, _ := auth.PrincipalFromContext(c)
|
|
id, ok := parseID(c, "id")
|
|
if !ok {
|
|
return
|
|
}
|
|
if !access.CanViewSOP(h.db, p, id) {
|
|
response.Error(c, http.StatusNotFound, "NOT_FOUND", "SOP 不存在")
|
|
return
|
|
}
|
|
var item model.SOP
|
|
var nodes []model.SOPNode
|
|
var edges []model.SOPEdge
|
|
item, nodes, edges, err := h.load(id, p.TenantID)
|
|
if err != nil {
|
|
response.Error(c, http.StatusNotFound, "NOT_FOUND", "SOP 不存在")
|
|
return
|
|
}
|
|
problems, err := h.validateForPublish(item, item.StartNodeKey, nodes, edges, p.TenantID)
|
|
if err != nil {
|
|
response.Error(c, http.StatusInternalServerError, "VALIDATION_FAILED", "校验流程失败")
|
|
return
|
|
}
|
|
response.OK(c, gin.H{"valid": len(problems) == 0, "problems": problems})
|
|
}
|
|
|
|
func (h *Handler) load(sopID, tenantID uint64) (model.SOP, []model.SOPNode, []model.SOPEdge, error) {
|
|
var item model.SOP
|
|
if err := h.db.Where("id = ? AND tenant_id = ?", sopID, tenantID).First(&item).Error; err != nil {
|
|
return item, nil, nil, err
|
|
}
|
|
nodes := make([]model.SOPNode, 0)
|
|
edges := make([]model.SOPEdge, 0)
|
|
if err := h.db.Where("sop_id = ? AND tenant_id = ?", sopID, tenantID).Order("position_y, id").Find(&nodes).Error; err != nil {
|
|
return item, nil, nil, err
|
|
}
|
|
if err := h.db.Where("sop_id = ? AND tenant_id = ?", sopID, tenantID).Order("priority, id").Find(&edges).Error; err != nil {
|
|
return item, nil, nil, err
|
|
}
|
|
return item, nodes, edges, nil
|
|
}
|
|
|
|
func (h *Handler) validateForPublish(item model.SOP, startNodeKey string, nodes []model.SOPNode, edges []model.SOPEdge, tenantID uint64) ([]string, error) {
|
|
fields := make([]model.ScenarioField, 0)
|
|
if err := h.db.Where("scenario_id = ? AND tenant_id = ?", item.ScenarioID, tenantID).Find(&fields).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
stageKeys := make([]string, 0)
|
|
if pkg, err := scriptkit.LoadPackageByScenario(h.db, tenantID, item.ScenarioID); err == nil {
|
|
for _, stage := range pkg.Stages {
|
|
stageKeys = append(stageKeys, stage.StageKey)
|
|
}
|
|
}
|
|
return ValidateForPublish(startNodeKey, nodes, edges, ValidationContext{Fields: fields, StageKeys: stageKeys}), nil
|
|
}
|
|
|
|
func toModels(tenantID, sopID uint64, input graphInput) ([]model.SOPNode, []model.SOPEdge) {
|
|
nodes := make([]model.SOPNode, 0, len(input.Nodes))
|
|
for _, item := range input.Nodes {
|
|
config := item.Config
|
|
if len(config) == 0 || !json.Valid(config) {
|
|
config = json.RawMessage(`{}`)
|
|
}
|
|
nodes = append(nodes, model.SOPNode{TenantID: tenantID, SOPID: sopID, NodeKey: item.NodeKey, Type: item.Type, Title: item.Title, Content: item.Content, Config: datatypes.JSON(config), PositionX: item.PositionX, PositionY: item.PositionY})
|
|
}
|
|
edges := make([]model.SOPEdge, 0, len(input.Edges))
|
|
for _, item := range input.Edges {
|
|
condition := item.Condition
|
|
if len(condition) == 0 || !json.Valid(condition) {
|
|
condition = json.RawMessage(`{}`)
|
|
}
|
|
edges = append(edges, model.SOPEdge{TenantID: tenantID, SOPID: sopID, SourceNodeKey: item.SourceNodeKey, TargetNodeKey: item.TargetNodeKey, Condition: datatypes.JSON(condition), Priority: item.Priority})
|
|
}
|
|
return nodes, edges
|
|
}
|
|
|
|
func parseID(c *gin.Context, name string) (uint64, bool) {
|
|
id, err := strconv.ParseUint(c.Param(name), 10, 64)
|
|
if err != nil || id == 0 {
|
|
response.Error(c, http.StatusBadRequest, "INVALID_ID", "资源 ID 不正确")
|
|
return 0, false
|
|
}
|
|
return id, true
|
|
}
|