feat: complete SOP version management
This commit is contained in:
@@ -2,8 +2,10 @@ package knowledge
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.iwork-ai.com/xdc/iqudo-top1/internal/access"
|
||||
"git.iwork-ai.com/xdc/iqudo-top1/internal/audit"
|
||||
@@ -13,6 +15,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type Handler struct{ db *gorm.DB }
|
||||
@@ -25,16 +28,22 @@ type input struct {
|
||||
Content map[string]interface{} `json:"content" binding:"required"`
|
||||
}
|
||||
|
||||
type updateInput struct {
|
||||
Title string `json:"title" binding:"required,max=128"`
|
||||
Content map[string]interface{} `json:"content" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *Handler) List(c *gin.Context) {
|
||||
p, _ := auth.PrincipalFromContext(c)
|
||||
type row struct {
|
||||
model.KnowledgeCard
|
||||
Content datatypes.JSON `json:"content"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
items := make([]row, 0)
|
||||
query := h.db.Table("knowledge_cards kc").Select("kc.*, kcv.content").Joins("JOIN scenarios sc ON sc.id = kc.scenario_id").Joins("LEFT JOIN knowledge_card_versions kcv ON kcv.knowledge_card_id = kc.id AND kcv.status = ?", "published")
|
||||
query := h.db.Table("knowledge_cards kc").Select("kc.*, kcv.content, kcv.version").Joins("JOIN scenarios sc ON sc.id = kc.scenario_id").Joins("LEFT JOIN knowledge_card_versions kcv ON kcv.knowledge_card_id = kc.id AND kcv.status = ?", "published")
|
||||
query = access.ScopeScenarios(query, p, "sc")
|
||||
err := query.Where("kc.tenant_id = ? AND kc.status <> ?", p.TenantID, "archived").Order("kc.updated_at DESC").Scan(&items).Error
|
||||
err := query.Where("kc.tenant_id = ? AND kc.status <> ? AND sc.status <> ?", p.TenantID, "archived", "archived").Order("kc.updated_at DESC").Scan(&items).Error
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询知识卡失败")
|
||||
return
|
||||
@@ -42,6 +51,78 @@ func (h *Handler) List(c *gin.Context) {
|
||||
response.OK(c, gin.H{"items": items, "total": len(items)})
|
||||
}
|
||||
|
||||
func (h *Handler) Update(c *gin.Context) {
|
||||
p, _ := auth.PrincipalFromContext(c)
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
response.Error(c, http.StatusBadRequest, "INVALID_ID", "知识卡 ID 不正确")
|
||||
return
|
||||
}
|
||||
var body updateInput
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "知识卡内容不完整")
|
||||
return
|
||||
}
|
||||
if err := validateContent(body.Content); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", err.Error())
|
||||
return
|
||||
}
|
||||
content, err := json.Marshal(body.Content)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "知识卡内容格式不正确")
|
||||
return
|
||||
}
|
||||
var card model.KnowledgeCard
|
||||
if err := h.db.Where("id = ? AND tenant_id = ? AND status <> ?", id, p.TenantID, "archived").First(&card).Error; err != nil || !access.CanEditScenario(h.db, p, card.ScenarioID) {
|
||||
response.Error(c, http.StatusNotFound, "NOT_FOUND", "知识卡不存在或不可编辑")
|
||||
return
|
||||
}
|
||||
var version model.KnowledgeCardVersion
|
||||
err = h.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND tenant_id = ?", id, p.TenantID).First(&card).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var maxVersion int
|
||||
if err := tx.Model(&model.KnowledgeCardVersion{}).Where("knowledge_card_id = ? AND tenant_id = ?", id, p.TenantID).Select("COALESCE(MAX(version), 0)").Scan(&maxVersion).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.KnowledgeCardVersion{}).Where("knowledge_card_id = ? AND tenant_id = ? AND status = ?", id, p.TenantID, "published").Update("status", "superseded").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
version = model.KnowledgeCardVersion{TenantID: p.TenantID, KnowledgeCardID: id, Version: maxVersion + 1, Content: datatypes.JSON(content), Status: "published"}
|
||||
if err := tx.Create(&version).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&card).Updates(map[string]interface{}{"title": body.Title, "status": "published"}).Error
|
||||
})
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "UPDATE_FAILED", "更新知识卡失败")
|
||||
return
|
||||
}
|
||||
_ = audit.Record(h.db, p, "update", "knowledge_card", id, gin.H{"version": version.Version})
|
||||
response.OK(c, gin.H{"id": id, "title": body.Title, "version": version.Version, "content": datatypes.JSON(content)})
|
||||
}
|
||||
|
||||
func (h *Handler) Versions(c *gin.Context) {
|
||||
p, _ := auth.PrincipalFromContext(c)
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
response.Error(c, http.StatusBadRequest, "INVALID_ID", "知识卡 ID 不正确")
|
||||
return
|
||||
}
|
||||
var card model.KnowledgeCard
|
||||
if err := h.db.Where("id = ? AND tenant_id = ? AND status <> ?", id, p.TenantID, "archived").First(&card).Error; err != nil || !access.CanViewScenario(h.db, p, card.ScenarioID) {
|
||||
response.Error(c, http.StatusNotFound, "NOT_FOUND", "知识卡不存在")
|
||||
return
|
||||
}
|
||||
items := make([]model.KnowledgeCardVersion, 0)
|
||||
if err := h.db.Where("knowledge_card_id = ? AND tenant_id = ?", id, p.TenantID).Order("version DESC").Find(&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) Create(c *gin.Context) {
|
||||
p, _ := auth.PrincipalFromContext(c)
|
||||
var body input
|
||||
@@ -49,6 +130,10 @@ func (h *Handler) Create(c *gin.Context) {
|
||||
response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "知识卡内容不完整")
|
||||
return
|
||||
}
|
||||
if err := validateContent(body.Content); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", err.Error())
|
||||
return
|
||||
}
|
||||
if !access.CanEditScenario(h.db, p, body.ScenarioID) {
|
||||
response.Error(c, http.StatusNotFound, "NOT_FOUND", "场景不存在或不可编辑")
|
||||
return
|
||||
@@ -103,3 +188,18 @@ func (h *Handler) Delete(c *gin.Context) {
|
||||
_ = audit.Record(h.db, p, "archive", "knowledge_card", id, nil)
|
||||
response.OK(c, gin.H{"id": id})
|
||||
}
|
||||
|
||||
func validateContent(content map[string]interface{}) error {
|
||||
standard, ok := content["standard_copy"].(string)
|
||||
if !ok || strings.TrimSpace(standard) == "" {
|
||||
return errors.New("请填写标准话术")
|
||||
}
|
||||
for _, key := range []string{"forbidden_copy", "risk_note"} {
|
||||
if value, exists := content[key]; exists {
|
||||
if _, ok := value.(string); !ok {
|
||||
return errors.New("知识卡文本字段格式不正确")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
23
internal/knowledge/handler_test.go
Normal file
23
internal/knowledge/handler_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package knowledge
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateContent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content map[string]interface{}
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "valid", content: map[string]interface{}{"standard_copy": "标准表达", "risk_note": "风险提示"}},
|
||||
{name: "missing standard copy", content: map[string]interface{}{"risk_note": "风险提示"}, wantErr: true},
|
||||
{name: "blank standard copy", content: map[string]interface{}{"standard_copy": " "}, wantErr: true},
|
||||
{name: "invalid risk note", content: map[string]interface{}{"standard_copy": "标准表达", "risk_note": 1}, wantErr: true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if err := validateContent(test.content); (err != nil) != test.wantErr {
|
||||
t.Fatalf("validateContent() error = %v, wantErr %v", err, test.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user