package sop import ( "encoding/json" "errors" "net/http" "strconv" "time" "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" "gorm.io/gorm/clause" ) 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"` } 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 var version model.SOPVersion err := h.db.Transaction(func(tx *gorm.DB) error { item = model.SOP{TenantID: p.TenantID, ScenarioID: scenarioID, Name: input.Name, Description: input.Description, Status: "draft", CreatedBy: p.UserID} if err := tx.Create(&item).Error; err != nil { return err } version = model.SOPVersion{TenantID: p.TenantID, SOPID: item.ID, Version: 1, Status: "draft", StartNodeKey: "start", CreatedBy: p.UserID} if err := tx.Create(&version).Error; err != nil { return err } nodes := []model.SOPNode{ {TenantID: p.TenantID, SOPVersionID: version.ID, NodeKey: "start", Type: "start", Title: "开始", Content: "", Config: datatypes.JSON([]byte(`{}`)), PositionX: 0, PositionY: 0}, {TenantID: p.TenantID, SOPVersionID: version.ID, NodeKey: "opening", Type: "message", Title: "开场", Content: "您好,我先了解一下具体情况。", Config: datatypes.JSON([]byte(`{}`)), PositionX: 0, PositionY: 120}, {TenantID: p.TenantID, SOPVersionID: version.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, SOPVersionID: version.ID, SourceNodeKey: "start", TargetNodeKey: "opening", Condition: datatypes.JSON([]byte(`{}`)), Priority: 0}, {TenantID: p.TenantID, SOPVersionID: version.ID, SourceNodeKey: "opening", TargetNodeKey: "finish", Condition: datatypes.JSON([]byte(`{}`)), Priority: 0}, } return tx.Create(&edges).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, "version": version}) } 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 version model.SOPVersion var nodes []model.SOPNode var edges []model.SOPEdge var err error if requested := c.Query("version"); requested != "" { versionNumber, parseErr := strconv.Atoi(requested) if parseErr != nil || versionNumber < 1 { response.Error(c, http.StatusBadRequest, "INVALID_VERSION", "SOP 版本不正确") return } item, version, nodes, edges, err = h.loadVersion(id, p.TenantID, versionNumber) } else { item, version, nodes, edges, err = h.loadLatest(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, "version": version, "nodes": nodes, "edges": edges}) } type versionItem struct { model.SOPVersion CreatorName string `json:"creator_name"` ReviewerName string `json:"reviewer_name"` } func (h *Handler) ListVersions(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 } items := make([]versionItem, 0) err := h.db.Table("sop_versions sv").Select("sv.*, creator.display_name AS creator_name, COALESCE(reviewer.display_name, '') AS reviewer_name"). Joins("JOIN users creator ON creator.id = sv.created_by"). Joins("LEFT JOIN users reviewer ON reviewer.id = sv.reviewed_by"). Where("sv.sop_id = ? AND sv.tenant_id = ?", id, p.TenantID). Order("sv.version DESC").Scan(&items).Error if err != nil { response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询版本历史失败") return } response.OK(c, gin.H{"items": items, "total": len(items)}) } 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 } var version model.SOPVersion if err := h.db.Where("sop_id = ? AND tenant_id = ? AND status = ?", id, p.TenantID, "draft").Order("version DESC").First(&version).Error; err != nil { response.Error(c, http.StatusConflict, "NO_DRAFT_VERSION", "没有可编辑的草稿版本") return } nodes, edges := toModels(p.TenantID, version.ID, input) problems := ValidateGraph(input.StartNodeKey, nodes, edges) 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_version_id = ?", version.ID).Delete(&model.SOPEdge{}).Error; err != nil { return err } if err := tx.Where("sop_version_id = ?", version.ID).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 } } return tx.Model(&version).Update("start_node_key", input.StartNodeKey).Error }) if err != nil { response.Error(c, http.StatusInternalServerError, "SAVE_FAILED", "保存流程失败") return } _ = audit.Record(h.db, p, "save_graph", "sop", id, gin.H{"version_id": version.ID}) 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 version model.SOPVersion var nodes []model.SOPNode var edges []model.SOPEdge var err error if requested := c.Query("version"); requested != "" { versionNumber, parseErr := strconv.Atoi(requested) if parseErr != nil || versionNumber < 1 { response.Error(c, http.StatusBadRequest, "INVALID_VERSION", "SOP 版本不正确") return } item, version, nodes, edges, err = h.loadVersion(id, p.TenantID, versionNumber) } else { item, version, nodes, edges, err = h.loadLatest(id, p.TenantID) } if err != nil { response.Error(c, http.StatusNotFound, "NOT_FOUND", "SOP 不存在") return } problems, err := h.validateForPublish(item, version.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) SubmitReview(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 } item, version, nodes, edges, err := h.loadLatest(id, p.TenantID) if err != nil || version.Status != "draft" { response.Error(c, http.StatusConflict, "NO_DRAFT_VERSION", "没有可提交审核的草稿版本") return } problems, validationErr := h.validateForPublish(item, version.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.Model(&version).Update("status", "reviewing").Error; err != nil { return err } var published int64 if err := tx.Model(&model.SOPVersion{}).Where("sop_id = ? AND tenant_id = ? AND status = ?", id, p.TenantID, "published").Count(&published).Error; err != nil { return err } if published == 0 { return tx.Model(&item).Update("status", "reviewing").Error } return nil }) if err != nil { response.Error(c, http.StatusInternalServerError, "SUBMIT_REVIEW_FAILED", "提交审核失败") return } _ = audit.Record(h.db, p, "submit_review", "sop", id, gin.H{"version": version.Version}) response.OK(c, gin.H{"id": id, "version": version.Version, "status": "reviewing"}) } func (h *Handler) Publish(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 } item, version, nodes, edges, err := h.loadLatest(id, p.TenantID) canDirectPublish := auth.HasPermission(p, "*") if err != nil || (version.Status != "reviewing" && !(version.Status == "draft" && canDirectPublish)) { response.Error(c, http.StatusConflict, "NO_REVIEW_VERSION", "没有可发布的审核版本") return } if !canDirectPublish && version.CreatedBy == p.UserID { response.Error(c, http.StatusForbidden, "SELF_REVIEW_FORBIDDEN", "不能审核并发布自己创建的 SOP") return } problems, validationErr := h.validateForPublish(item, version.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 } now := time.Now() err = h.db.Transaction(func(tx *gorm.DB) error { if err := BindKnowledgeVersions(tx, p.TenantID, version.ID); err != nil { return err } if err := tx.Model(&model.SOPVersion{}).Where("sop_id = ? AND tenant_id = ? AND status = ?", id, p.TenantID, "published").Update("status", "superseded").Error; err != nil { return err } if err := tx.Model(&version).Updates(map[string]interface{}{"status": "published", "published_at": &now, "reviewed_by": p.UserID}).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, "PUBLISH_FAILED", "发布 SOP 失败") return } _ = audit.Record(h.db, p, "publish", "sop", id, gin.H{"version": version.Version}) response.OK(c, gin.H{"id": id, "version": version.Version, "published_at": now}) } func (h *Handler) Offline(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 if err := h.db.Where("id = ? AND tenant_id = ? AND status = ?", id, p.TenantID, "published").First(&item).Error; err != nil { response.Error(c, http.StatusNotFound, "NOT_FOUND", "已发布 SOP 不存在") return } err := h.db.Transaction(func(tx *gorm.DB) error { if err := tx.Model(&item).Update("status", "offline").Error; err != nil { return err } if err := tx.Model(&model.SOPVersion{}).Where("sop_id = ? AND tenant_id = ? AND status = ?", id, p.TenantID, "published").Update("status", "offline").Error; err != nil { return err } var published int64 if err := tx.Model(&model.SOP{}).Where("scenario_id = ? AND tenant_id = ? AND status = ?", item.ScenarioID, p.TenantID, "published").Count(&published).Error; err != nil { return err } if published == 0 { return tx.Model(&model.Scenario{}).Where("id = ? AND tenant_id = ?", item.ScenarioID, p.TenantID).Update("status", "draft").Error } return nil }) if err != nil { response.Error(c, http.StatusInternalServerError, "OFFLINE_FAILED", "下线 SOP 失败") return } _ = audit.Record(h.db, p, "offline", "sop", id, nil) response.OK(c, gin.H{"id": id, "status": "offline"}) } func (h *Handler) CreateVersion(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 existing int64 h.db.Model(&model.SOPVersion{}).Where("sop_id = ? AND tenant_id = ? AND status IN ?", id, p.TenantID, []string{"draft", "reviewing"}).Count(&existing) if existing > 0 { response.Error(c, http.StatusConflict, "DRAFT_EXISTS", "已经存在草稿或审核中的版本") return } _, source, nodes, edges, err := h.loadLatest(id, p.TenantID) if err != nil { response.Error(c, http.StatusNotFound, "NOT_FOUND", "SOP 不存在") return } var version model.SOPVersion err = h.db.Transaction(func(tx *gorm.DB) error { version = model.SOPVersion{TenantID: p.TenantID, SOPID: id, Version: source.Version + 1, Status: "draft", StartNodeKey: source.StartNodeKey, CreatedBy: p.UserID} if err := tx.Create(&version).Error; err != nil { return err } for i := range nodes { nodes[i].Base = model.Base{} nodes[i].SOPVersionID = version.ID } for i := range edges { edges[i].Base = model.Base{} edges[i].SOPVersionID = version.ID } if err := tx.Create(&nodes).Error; err != nil { return err } if len(edges) > 0 { return tx.Create(&edges).Error } return nil }) if err != nil { response.Error(c, http.StatusInternalServerError, "CREATE_VERSION_FAILED", "创建草稿版本失败") return } _ = audit.Record(h.db, p, "create_version", "sop", id, gin.H{"version": version.Version}) response.Created(c, version) } func (h *Handler) Rollback(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 input struct { Version int `json:"version" binding:"required,min=1"` } if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "请选择要回滚的历史版本") return } item, source, nodes, edges, err := h.loadVersion(id, p.TenantID, input.Version) if err != nil || (source.Status != "superseded" && source.Status != "offline") { response.Error(c, http.StatusConflict, "INVALID_ROLLBACK_VERSION", "只能回滚到已替换或已下线的历史版本") return } problems, validationErr := h.validateForPublish(item, source.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 } now := time.Now() var restored model.SOPVersion pendingVersionErr := errors.New("存在草稿或审核中的版本,请先处理后再回滚") err = h.db.Transaction(func(tx *gorm.DB) error { var locked model.SOP if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND tenant_id = ?", id, p.TenantID).First(&locked).Error; err != nil { return err } var pending int64 if err := tx.Model(&model.SOPVersion{}).Where("sop_id = ? AND tenant_id = ? AND status IN ?", id, p.TenantID, []string{"draft", "reviewing"}).Count(&pending).Error; err != nil { return err } if pending > 0 { return pendingVersionErr } var maxVersion int if err := tx.Model(&model.SOPVersion{}).Where("sop_id = ? AND tenant_id = ?", id, p.TenantID).Select("COALESCE(MAX(version), 0)").Scan(&maxVersion).Error; err != nil { return err } if err := tx.Model(&model.SOPVersion{}).Where("sop_id = ? AND tenant_id = ? AND status = ?", id, p.TenantID, "published").Update("status", "superseded").Error; err != nil { return err } reviewerID := p.UserID restored = model.SOPVersion{TenantID: p.TenantID, SOPID: id, Version: maxVersion + 1, Status: "published", StartNodeKey: source.StartNodeKey, PublishedAt: &now, CreatedBy: p.UserID, ReviewedBy: &reviewerID} if err := tx.Create(&restored).Error; err != nil { return err } if err := cloneGraph(tx, nodes, edges, restored.ID); err != nil { return err } if err := tx.Model(&locked).Update("status", "published").Error; err != nil { return err } return tx.Model(&model.Scenario{}).Where("id = ? AND tenant_id = ?", locked.ScenarioID, p.TenantID).Update("status", "active").Error }) if err != nil { if errors.Is(err, pendingVersionErr) { response.Error(c, http.StatusConflict, "PENDING_VERSION_EXISTS", err.Error()) return } response.Error(c, http.StatusInternalServerError, "ROLLBACK_FAILED", "回滚 SOP 失败") return } _ = audit.Record(h.db, p, "rollback", "sop", id, gin.H{"source_version": source.Version, "new_version": restored.Version}) response.Created(c, gin.H{"id": id, "source_version": source.Version, "version": restored.Version, "published_at": now}) } func (h *Handler) loadLatest(sopID, tenantID uint64) (model.SOP, model.SOPVersion, []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, model.SOPVersion{}, nil, nil, err } var version model.SOPVersion if err := h.db.Where("sop_id = ? AND tenant_id = ?", sopID, tenantID).Order("version DESC").First(&version).Error; err != nil { return item, version, nil, nil, err } nodes := make([]model.SOPNode, 0) edges := make([]model.SOPEdge, 0) if err := h.db.Where("sop_version_id = ? AND tenant_id = ?", version.ID, tenantID).Order("position_y, id").Find(&nodes).Error; err != nil { return item, version, nil, nil, err } if err := h.db.Where("sop_version_id = ? AND tenant_id = ?", version.ID, tenantID).Order("priority, id").Find(&edges).Error; err != nil { return item, version, nil, nil, err } return item, version, nodes, edges, nil } func (h *Handler) loadVersion(sopID, tenantID uint64, versionNumber int) (model.SOP, model.SOPVersion, []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, model.SOPVersion{}, nil, nil, err } var version model.SOPVersion if err := h.db.Where("sop_id = ? AND tenant_id = ? AND version = ?", sopID, tenantID, versionNumber).First(&version).Error; err != nil { return item, version, nil, nil, err } nodes := make([]model.SOPNode, 0) edges := make([]model.SOPEdge, 0) if err := h.db.Where("sop_version_id = ? AND tenant_id = ?", version.ID, tenantID).Order("position_y, id").Find(&nodes).Error; err != nil { return item, version, nil, nil, err } if err := h.db.Where("sop_version_id = ? AND tenant_id = ?", version.ID, tenantID).Order("priority, id").Find(&edges).Error; err != nil { return item, version, nil, nil, err } return item, version, nodes, edges, nil } func cloneGraph(tx *gorm.DB, nodes []model.SOPNode, edges []model.SOPEdge, versionID uint64) error { for i := range nodes { nodes[i].Base = model.Base{} nodes[i].SOPVersionID = versionID } for i := range edges { edges[i].Base = model.Base{} edges[i].SOPVersionID = versionID } if len(nodes) > 0 { if err := tx.Create(&nodes).Error; err != nil { return err } } if len(edges) > 0 { return tx.Create(&edges).Error } return 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 } var cards []model.KnowledgeCard if err := h.db.Where("scenario_id = ? AND tenant_id = ? AND status = ?", item.ScenarioID, tenantID, "published").Find(&cards).Error; err != nil { return nil, err } cardIDs := make(map[uint64]bool, len(cards)) for _, card := range cards { cardIDs[card.ID] = true } return ValidateForPublish(startNodeKey, nodes, edges, ValidationContext{Fields: fields, PublishedKnowledgeCardIDs: cardIDs}), nil } func toModels(tenantID, versionID 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, SOPVersionID: versionID, 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, SOPVersionID: versionID, 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 }