feat: complete pet SOP governance workflow
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"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"
|
||||
@@ -54,16 +55,17 @@ type edgeInput struct {
|
||||
|
||||
func (h *Handler) List(c *gin.Context) {
|
||||
p, _ := auth.PrincipalFromContext(c)
|
||||
var items []model.SOP
|
||||
query := h.db.Where("tenant_id = ? AND status <> ?", p.TenantID, "archived")
|
||||
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("scenario_id = ?", scenarioID)
|
||||
query = query.Where("s.scenario_id = ?", scenarioID)
|
||||
}
|
||||
if err := query.Order("updated_at DESC").Find(&items).Error; err != nil {
|
||||
if err := query.Order("s.updated_at DESC").Scan(&items).Error; err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询 SOP 失败")
|
||||
return
|
||||
}
|
||||
@@ -76,6 +78,10 @@ func (h *Handler) Create(c *gin.Context) {
|
||||
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", "场景不存在")
|
||||
@@ -125,6 +131,10 @@ func (h *Handler) Get(c *gin.Context) {
|
||||
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)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -143,6 +153,10 @@ func (h *Handler) SaveGraph(c *gin.Context) {
|
||||
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", "流程配置不完整")
|
||||
@@ -190,6 +204,10 @@ func (h *Handler) Validate(c *gin.Context) {
|
||||
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)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusNotFound, "NOT_FOUND", "SOP 不存在")
|
||||
@@ -209,6 +227,10 @@ func (h *Handler) SubmitReview(c *gin.Context) {
|
||||
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", "没有可提交审核的草稿版本")
|
||||
@@ -227,7 +249,14 @@ func (h *Handler) SubmitReview(c *gin.Context) {
|
||||
if err := tx.Model(&version).Update("status", "reviewing").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&item).Update("status", "reviewing").Error
|
||||
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", "提交审核失败")
|
||||
@@ -243,11 +272,20 @@ func (h *Handler) Publish(c *gin.Context) {
|
||||
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)
|
||||
if err != nil || (version.Status != "draft" && version.Status != "reviewing") {
|
||||
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", "校验流程失败")
|
||||
@@ -284,6 +322,10 @@ func (h *Handler) Offline(c *gin.Context) {
|
||||
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 不存在")
|
||||
@@ -319,10 +361,14 @@ func (h *Handler) CreateVersion(c *gin.Context) {
|
||||
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 = ?", id, p.TenantID, "draft").Count(&existing)
|
||||
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", "已经存在草稿版本")
|
||||
response.Error(c, http.StatusConflict, "DRAFT_EXISTS", "已经存在草稿或审核中的版本")
|
||||
return
|
||||
}
|
||||
_, source, nodes, edges, err := h.loadLatest(id, p.TenantID)
|
||||
@@ -369,8 +415,8 @@ func (h *Handler) loadLatest(sopID, tenantID uint64) (model.SOP, model.SOPVersio
|
||||
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
|
||||
}
|
||||
var nodes []model.SOPNode
|
||||
var edges []model.SOPEdge
|
||||
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
|
||||
}
|
||||
@@ -381,7 +427,7 @@ func (h *Handler) loadLatest(sopID, tenantID uint64) (model.SOP, model.SOPVersio
|
||||
}
|
||||
|
||||
func (h *Handler) validateForPublish(item model.SOP, startNodeKey string, nodes []model.SOPNode, edges []model.SOPEdge, tenantID uint64) ([]string, error) {
|
||||
var fields []model.ScenarioField
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user