package sop import ( "net/http" "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/gorm" ) type ReviewItem struct { SOPID uint64 `json:"sop_id"` SOPName string `json:"sop_name"` Description string `json:"description"` ScenarioID uint64 `json:"scenario_id"` ScenarioName string `json:"scenario_name"` VersionID uint64 `json:"version_id"` Version int `json:"version"` CreatorID uint64 `json:"creator_id"` CreatorName string `json:"creator_name"` SubmittedAt time.Time `json:"submitted_at"` } func (h *Handler) Reviews(c *gin.Context) { principal, _ := auth.PrincipalFromContext(c) items := make([]ReviewItem, 0) query := h.db.Table("sop_versions sv").Select( "s.id AS sop_id, s.name AS sop_name, s.description, sc.id AS scenario_id, sc.name AS scenario_name, " + "sv.id AS version_id, sv.version, sv.created_by AS creator_id, u.display_name AS creator_name, sv.updated_at AS submitted_at", ).Joins("JOIN sops s ON s.id = sv.sop_id").Joins("JOIN scenarios sc ON sc.id = s.scenario_id").Joins("JOIN users u ON u.id = sv.created_by") query = access.ScopeScenarios(query, principal, "sc") if err := query.Where("sv.tenant_id = ? AND sv.status = ?", principal.TenantID, "reviewing").Order("sv.updated_at ASC").Scan(&items).Error; err != nil { response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询审核任务失败") return } response.OK(c, gin.H{"items": items, "total": len(items)}) } func (h *Handler) Reject(c *gin.Context) { principal, _ := auth.PrincipalFromContext(c) sopID, ok := parseID(c, "id") if !ok { return } if !access.CanViewSOP(h.db, principal, sopID) { response.Error(c, http.StatusNotFound, "NOT_FOUND", "SOP 不存在") return } var input struct { Reason string `json:"reason" binding:"required,max=1000"` } 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 = ?", sopID, principal.TenantID, "reviewing").Order("version DESC").First(&version).Error; err != nil { response.Error(c, http.StatusConflict, "NO_REVIEW_VERSION", "没有待审核版本") return } if !auth.HasPermission(principal, "*") && version.CreatedBy == principal.UserID { response.Error(c, http.StatusForbidden, "SELF_REVIEW_FORBIDDEN", "不能审核自己创建的 SOP") return } err := h.db.Transaction(func(tx *gorm.DB) error { if err := tx.Model(&version).Updates(map[string]interface{}{"status": "draft", "reviewed_by": nil}).Error; err != nil { return err } var published int64 if err := tx.Model(&model.SOPVersion{}).Where("sop_id = ? AND tenant_id = ? AND status = ?", sopID, principal.TenantID, "published").Count(&published).Error; err != nil { return err } status := "draft" if published > 0 { status = "published" } return tx.Model(&model.SOP{}).Where("id = ? AND tenant_id = ?", sopID, principal.TenantID).Update("status", status).Error }) if err != nil { response.Error(c, http.StatusInternalServerError, "REJECT_FAILED", "退回审核失败") return } _ = audit.Record(h.db, principal, "reject", "sop", sopID, gin.H{"version": version.Version, "reason": input.Reason}) response.OK(c, gin.H{"id": sopID, "version": version.Version, "status": "draft"}) }