feat: complete pet SOP governance workflow

This commit is contained in:
Eric 1549169735@qq.com
2026-08-08 22:58:35 +08:00
parent 97a76250f7
commit 4b240499c2
85 changed files with 1316 additions and 269 deletions

View File

@@ -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
}