feat: complete SOP execution workflow
This commit is contained in:
@@ -23,6 +23,19 @@ type Handler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type listItem struct {
|
||||
model.SOPRun
|
||||
SOPName string `json:"sop_name"`
|
||||
ScenarioName string `json:"scenario_name"`
|
||||
Version int `json:"version"`
|
||||
OperatorName string `json:"operator_name"`
|
||||
}
|
||||
|
||||
type filterOption struct {
|
||||
Value uint64 `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
func NewHandler(db *gorm.DB) *Handler {
|
||||
return &Handler{db: db}
|
||||
}
|
||||
@@ -101,12 +114,12 @@ func (h *Handler) Get(c *gin.Context) {
|
||||
|
||||
func (h *Handler) List(c *gin.Context) {
|
||||
p, _ := auth.PrincipalFromContext(c)
|
||||
type row struct {
|
||||
model.SOPRun
|
||||
SOPName string `json:"sop_name"`
|
||||
}
|
||||
page, pageSize := pagination(c)
|
||||
query := scopeRuns(h.db.Table("sop_runs r").Joins("JOIN sops s ON s.id = r.sop_id"), p, "r")
|
||||
query := scopeRuns(h.db.Table("sop_runs r").
|
||||
Joins("JOIN sops s ON s.id = r.sop_id").
|
||||
Joins("JOIN scenarios sc ON sc.id = s.scenario_id").
|
||||
Joins("JOIN sop_versions sv ON sv.id = r.sop_version_id").
|
||||
Joins("JOIN users u ON u.id = r.operator_id"), p, "r")
|
||||
if status := c.Query("status"); status != "" {
|
||||
query = query.Where("r.status = ?", status)
|
||||
}
|
||||
@@ -116,6 +129,9 @@ func (h *Handler) List(c *gin.Context) {
|
||||
if sopID := c.Query("sop_id"); sopID != "" {
|
||||
query = query.Where("r.sop_id = ?", sopID)
|
||||
}
|
||||
if operatorID := c.Query("operator_id"); operatorID != "" {
|
||||
query = query.Where("r.operator_id = ?", operatorID)
|
||||
}
|
||||
if startedFrom := c.Query("started_from"); startedFrom != "" {
|
||||
query = query.Where("r.started_at >= ?", startedFrom)
|
||||
}
|
||||
@@ -127,14 +143,37 @@ func (h *Handler) List(c *gin.Context) {
|
||||
response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询执行记录失败")
|
||||
return
|
||||
}
|
||||
items := make([]row, 0)
|
||||
if err := query.Select("r.*, s.name AS sop_name").Order("r.created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Scan(&items).Error; err != nil {
|
||||
items := make([]listItem, 0)
|
||||
if err := query.Select("r.*, s.name AS sop_name, sc.name AS scenario_name, sv.version, u.display_name AS operator_name").Order("r.created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Scan(&items).Error; err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询执行记录失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": items, "total": total, "page": page, "page_size": pageSize})
|
||||
}
|
||||
|
||||
func (h *Handler) Options(c *gin.Context) {
|
||||
p, _ := auth.PrincipalFromContext(c)
|
||||
sops := make([]filterOption, 0)
|
||||
sopQuery := scopeRuns(h.db.Table("sop_runs r"), p, "r").
|
||||
Select("DISTINCT s.id AS value, s.name AS label").
|
||||
Joins("JOIN sops s ON s.id = r.sop_id").
|
||||
Order("s.name, s.id")
|
||||
if err := sopQuery.Scan(&sops).Error; err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询 SOP 筛选项失败")
|
||||
return
|
||||
}
|
||||
operators := make([]filterOption, 0)
|
||||
operatorQuery := scopeRuns(h.db.Table("sop_runs r"), p, "r").
|
||||
Select("DISTINCT u.id AS value, u.display_name AS label").
|
||||
Joins("JOIN users u ON u.id = r.operator_id").
|
||||
Order("u.display_name, u.id")
|
||||
if err := operatorQuery.Scan(&operators).Error; err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询执行人筛选项失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"sops": sops, "operators": operators})
|
||||
}
|
||||
|
||||
func (h *Handler) Answer(c *gin.Context) {
|
||||
p, _ := auth.PrincipalFromContext(c)
|
||||
id, ok := runID(c)
|
||||
@@ -185,7 +224,7 @@ func (h *Handler) Answer(c *gin.Context) {
|
||||
if err := tx.Where("sop_version_id = ? AND source_node_key = ?", updated.SOPVersionID, updated.CurrentNodeKey).Order("priority, id").Find(&edges).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
sort.SliceStable(edges, func(i, j int) bool { return edges[i].Priority < edges[j].Priority })
|
||||
sortEdges(edges)
|
||||
nextKey := ""
|
||||
for _, edge := range edges {
|
||||
matched, matchErr := matchCondition(json.RawMessage(edge.Condition), answers)
|
||||
@@ -233,6 +272,29 @@ func (h *Handler) Answer(c *gin.Context) {
|
||||
h.respondRun(c, updated)
|
||||
}
|
||||
|
||||
func sortEdges(edges []model.SOPEdge) {
|
||||
sort.SliceStable(edges, func(i, j int) bool {
|
||||
leftDefault := defaultCondition(edges[i].Condition)
|
||||
rightDefault := defaultCondition(edges[j].Condition)
|
||||
if leftDefault != rightDefault {
|
||||
return !leftDefault
|
||||
}
|
||||
return edges[i].Priority < edges[j].Priority
|
||||
})
|
||||
}
|
||||
|
||||
func defaultCondition(raw []byte) bool {
|
||||
if len(raw) == 0 {
|
||||
return true
|
||||
}
|
||||
var value interface{}
|
||||
if err := json.Unmarshal(raw, &value); err != nil || value == nil {
|
||||
return err == nil && value == nil
|
||||
}
|
||||
object, ok := value.(map[string]interface{})
|
||||
return ok && len(object) == 0
|
||||
}
|
||||
|
||||
func (h *Handler) Finish(c *gin.Context) {
|
||||
p, _ := auth.PrincipalFromContext(c)
|
||||
id, ok := runID(c)
|
||||
@@ -240,29 +302,51 @@ func (h *Handler) Finish(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
var input struct {
|
||||
Result string `json:"result" binding:"required,max=64"`
|
||||
Result string `json:"result" binding:"required,oneof=manual"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "请选择执行结果")
|
||||
return
|
||||
}
|
||||
var run model.SOPRun
|
||||
if err := h.db.Where("id = ? AND tenant_id = ?", id, p.TenantID).First(&run).Error; err != nil || !canOperateRun(p, run) {
|
||||
runCompletedErr := errors.New("执行记录已经结束")
|
||||
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(&run).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if !canOperateRun(p, run) {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
if run.Status != "running" {
|
||||
return runCompletedErr
|
||||
}
|
||||
now := time.Now()
|
||||
payload, _ := json.Marshal(input)
|
||||
if err := tx.Create(&model.SOPRunEvent{TenantID: p.TenantID, RunID: id, NodeKey: run.CurrentNodeKey, Action: "finish", Payload: datatypes.JSON(payload)}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&run).Updates(map[string]interface{}{"status": "completed", "result": input.Result, "completed_at": &now}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
run.Status = "completed"
|
||||
run.Result = input.Result
|
||||
run.CompletedAt = &now
|
||||
return nil
|
||||
})
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.Error(c, http.StatusNotFound, "NOT_FOUND", "执行记录不存在")
|
||||
return
|
||||
}
|
||||
if run.Status != "running" {
|
||||
response.Error(c, http.StatusConflict, "RUN_COMPLETED", "执行记录已经结束")
|
||||
if errors.Is(err, runCompletedErr) {
|
||||
response.Error(c, http.StatusConflict, "RUN_COMPLETED", err.Error())
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
result := h.db.Model(&model.SOPRun{}).Where("id = ? AND tenant_id = ? AND status = ?", id, p.TenantID, "running").Updates(map[string]interface{}{"status": "completed", "result": input.Result, "completed_at": &now})
|
||||
if result.Error != nil || result.RowsAffected == 0 {
|
||||
response.Error(c, http.StatusNotFound, "NOT_FOUND", "执行记录不存在")
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "FINISH_FAILED", "结束执行失败")
|
||||
return
|
||||
}
|
||||
_ = audit.Record(h.db, p, "finish", "sop_run", id, input)
|
||||
h.Get(c)
|
||||
h.respondRun(c, run)
|
||||
}
|
||||
|
||||
func (h *Handler) Feedback(c *gin.Context) {
|
||||
@@ -299,13 +383,21 @@ func (h *Handler) Feedback(c *gin.Context) {
|
||||
|
||||
func (h *Handler) respondRun(c *gin.Context, item model.SOPRun) {
|
||||
var node model.SOPNode
|
||||
if err := h.db.Where("sop_version_id = ? AND node_key = ?", item.SOPVersionID, item.CurrentNodeKey).First(&node).Error; err != nil {
|
||||
if err := h.db.Where("sop_version_id = ? AND node_key = ? AND tenant_id = ?", item.SOPVersionID, item.CurrentNodeKey, item.TenantID).First(&node).Error; err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "NODE_NOT_FOUND", "当前流程节点不存在")
|
||||
return
|
||||
}
|
||||
nodeView, err := h.nodeView(h.db, node, item.TenantID)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "KNOWLEDGE_NOT_FOUND", "当前节点关联的知识卡版本不存在")
|
||||
return
|
||||
}
|
||||
fields := make([]model.ScenarioField, 0)
|
||||
h.db.Table("scenario_fields sf").Joins("JOIN sops s ON s.scenario_id = sf.scenario_id").Where("s.id = ? AND sf.tenant_id = ?", item.SOPID, item.TenantID).Order("sf.sort_order, sf.id").Find(&fields)
|
||||
response.OK(c, gin.H{"run": item, "node": node, "fields": fields})
|
||||
if err := h.db.Table("scenario_fields sf").Joins("JOIN sops s ON s.scenario_id = sf.scenario_id").Where("s.id = ? AND sf.tenant_id = ?", item.SOPID, item.TenantID).Order("sf.sort_order, sf.id").Find(&fields).Error; err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询场景字段失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"run": item, "node": nodeView, "fields": fields})
|
||||
}
|
||||
|
||||
func runID(c *gin.Context) (uint64, bool) {
|
||||
|
||||
Reference in New Issue
Block a user