feat: simplify SOP to immediate-effect configuration
This commit is contained in:
335
internal/multitable/projector.go
Normal file
335
internal/multitable/projector.go
Normal file
@@ -0,0 +1,335 @@
|
||||
package multitable
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.iwork-ai.com/xdc/iqudo-top1/internal/config"
|
||||
"git.iwork-ai.com/xdc/iqudo-top1/internal/model"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Projector struct {
|
||||
db *gorm.DB
|
||||
client *Client
|
||||
tables config.MultiTableTables
|
||||
}
|
||||
|
||||
func NewProjector(db *gorm.DB, client *Client, tables config.MultiTableTables) *Projector {
|
||||
return &Projector{db: db, client: client, tables: tables}
|
||||
}
|
||||
|
||||
func (p *Projector) Project(ctx context.Context, event model.MultiTableOutbox) error {
|
||||
switch event.Resource {
|
||||
case "scenario":
|
||||
return p.scenario(ctx, event)
|
||||
case "scenario_field":
|
||||
return p.scenarioField(ctx, event)
|
||||
case "scenario_rule":
|
||||
return p.scenarioRule(ctx, event)
|
||||
case "sop":
|
||||
return p.sop(ctx, event)
|
||||
case "knowledge_item":
|
||||
return p.knowledgeItem(ctx, event)
|
||||
case "knowledge_relation":
|
||||
return p.knowledgeRelation(ctx, event)
|
||||
case "sop_run":
|
||||
if err := p.run(ctx, event); err != nil {
|
||||
return err
|
||||
}
|
||||
if event.Action == "feedback" {
|
||||
return p.feedback(ctx, event)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Projector) scenario(ctx context.Context, event model.MultiTableOutbox) error {
|
||||
var item model.Scenario
|
||||
if err := p.db.Where("id = ? AND tenant_id = ?", event.ResourceID, event.TenantID).First(&item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.client.Upsert(ctx, p.tables.Scenarios, id(item.ID), common(item.ID, item.TenantID, statusFor(item.Status), item.UpdatedAt, map[string]interface{}{
|
||||
"名称": item.Name, "行业": item.Industry, "适用角色": item.RoleName, "目标": clip(item.Goal), "触发条件": clip(item.TriggerText), "可见范围": item.Visibility, "业务状态": item.Status, "创建人ID": id(item.CreatedBy),
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
if item.Status != "archived" {
|
||||
return nil
|
||||
}
|
||||
fields := make([]model.ScenarioField, 0)
|
||||
if err := p.db.Where("scenario_id = ? AND tenant_id = ?", item.ID, item.TenantID).Find(&fields).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, field := range fields {
|
||||
if err := p.projectScenarioField(ctx, field, "已归档"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
sops := make([]model.SOP, 0)
|
||||
if err := p.db.Where("scenario_id = ? AND tenant_id = ?", item.ID, item.TenantID).Find(&sops).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, sop := range sops {
|
||||
if err := p.sop(ctx, model.MultiTableOutbox{TenantID: item.TenantID, ResourceID: sop.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Projector) scenarioField(ctx context.Context, event model.MultiTableOutbox) error {
|
||||
var item model.ScenarioField
|
||||
if err := p.db.Where("id = ? AND tenant_id = ?", event.ResourceID, event.TenantID).First(&item).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) && event.Action == "delete" {
|
||||
return p.deletedScenarioField(ctx, event)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return p.projectScenarioField(ctx, item, "正常")
|
||||
}
|
||||
|
||||
func (p *Projector) projectScenarioField(ctx context.Context, item model.ScenarioField, syncStatus string) error {
|
||||
return p.client.Upsert(ctx, p.tables.ScenarioFields, id(item.ID), common(item.ID, item.TenantID, syncStatus, item.UpdatedAt, map[string]interface{}{
|
||||
"场景来源ID": id(item.ScenarioID), "字段标识": item.FieldKey, "字段名称": item.FieldName, "外部数据路径": item.SourcePath, "字段类型": item.FieldType, "是否必填": yesNo(item.Required), "选项": jsonText(item.Options), "校验规则": jsonText(item.Validation), "排序": item.SortOrder,
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *Projector) scenarioRule(ctx context.Context, event model.MultiTableOutbox) error {
|
||||
var item model.ScenarioRule
|
||||
err := p.db.Where("id = ? AND tenant_id = ?", event.ResourceID, event.TenantID).First(&item).Error
|
||||
if err == nil {
|
||||
return p.client.Upsert(ctx, p.tables.ScenarioRules, id(item.ID), scenarioRuleProjection(item, "正常"))
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) || event.Action != "archive" {
|
||||
return err
|
||||
}
|
||||
var payload struct {
|
||||
ScenarioID uint64 `json:"scenario_id"`
|
||||
RuleKey string `json:"rule_key"`
|
||||
Name string `json:"name"`
|
||||
Priority int `json:"priority"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := json.Unmarshal(event.Payload, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.client.Upsert(ctx, p.tables.ScenarioRules, id(event.ResourceID), common(event.ResourceID, event.TenantID, "已归档", event.UpdatedAt, map[string]interface{}{
|
||||
"场景来源ID": id(payload.ScenarioID), "规则标识": payload.RuleKey, "名称": payload.Name, "优先级": payload.Priority, "规则状态": payload.Status,
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *Projector) deletedScenarioField(ctx context.Context, event model.MultiTableOutbox) error {
|
||||
var payload struct {
|
||||
ScenarioID uint64 `json:"scenario_id"`
|
||||
FieldKey string `json:"field_key"`
|
||||
FieldName string `json:"field_name"`
|
||||
SourcePath string `json:"source_path"`
|
||||
FieldType string `json:"field_type"`
|
||||
Required bool `json:"required"`
|
||||
Options json.RawMessage `json:"options"`
|
||||
Validation json.RawMessage `json:"validation"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
if err := json.Unmarshal(event.Payload, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
if payload.ScenarioID == 0 || payload.FieldKey == "" || payload.FieldName == "" || payload.FieldType == "" {
|
||||
return errors.New("deleted scenario field audit payload is incomplete")
|
||||
}
|
||||
item := model.ScenarioField{Base: model.Base{ID: event.ResourceID, UpdatedAt: event.UpdatedAt}, TenantID: event.TenantID, ScenarioID: payload.ScenarioID, FieldKey: payload.FieldKey, FieldName: payload.FieldName, SourcePath: payload.SourcePath, FieldType: payload.FieldType, Required: payload.Required, Options: datatypes.JSON(payload.Options), Validation: datatypes.JSON(payload.Validation), SortOrder: payload.SortOrder}
|
||||
return p.projectScenarioField(ctx, item, "已归档")
|
||||
}
|
||||
|
||||
func (p *Projector) sop(ctx context.Context, event model.MultiTableOutbox) error {
|
||||
var sop model.SOP
|
||||
if err := p.db.Where("id = ? AND tenant_id = ?", event.ResourceID, event.TenantID).First(&sop).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
versions := make([]model.SOPVersion, 0)
|
||||
if err := p.db.Where("sop_id = ? AND tenant_id = ? AND status IN ?", sop.ID, sop.TenantID, []string{"published", "superseded", "offline"}).Find(&versions).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, version := range versions {
|
||||
if err := p.sopVersion(ctx, sop, version); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Projector) sopVersion(ctx context.Context, sop model.SOP, version model.SOPVersion) error {
|
||||
nodes := make([]model.SOPNode, 0)
|
||||
edges := make([]model.SOPEdge, 0)
|
||||
if err := p.db.Where("sop_version_id = ?", version.ID).Order("id").Find(&nodes).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.db.Where("sop_version_id = ?", version.ID).Order("priority, id").Find(&edges).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
state := statusFor(version.Status)
|
||||
if sop.Status == "archived" {
|
||||
state = "已归档"
|
||||
}
|
||||
if err := p.client.Upsert(ctx, p.tables.SOPVersions, id(version.ID), common(version.ID, version.TenantID, state, version.UpdatedAt, map[string]interface{}{
|
||||
"SOP来源ID": id(sop.ID), "场景来源ID": id(sop.ScenarioID), "SOP名称": sop.Name, "版本号": version.Version, "版本状态": version.Status, "开始节点": version.StartNodeKey, "发布时间": formatTime(version.PublishedAt), "节点数": len(nodes), "路径数": len(edges),
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if err := p.client.Upsert(ctx, p.tables.SOPNodes, id(node.ID), common(node.ID, node.TenantID, state, node.UpdatedAt, map[string]interface{}{
|
||||
"SOP版本来源ID": id(version.ID), "节点标识": node.NodeKey, "节点类型": node.Type, "标题": node.Title, "标准话术或操作提示": clip(node.Content), "配置": jsonText(node.Config), "排序": node.PositionY,
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, edge := range edges {
|
||||
if err := p.client.Upsert(ctx, p.tables.SOPEdges, id(edge.ID), common(edge.ID, edge.TenantID, state, edge.UpdatedAt, map[string]interface{}{
|
||||
"SOP版本来源ID": id(version.ID), "起点节点标识": edge.SourceNodeKey, "终点节点标识": edge.TargetNodeKey, "条件": jsonText(edge.Condition), "优先级": edge.Priority,
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Projector) knowledgeItem(ctx context.Context, event model.MultiTableOutbox) error {
|
||||
var item model.KnowledgeItem
|
||||
err := p.db.Where("id = ? AND tenant_id = ?", event.ResourceID, event.TenantID).First(&item).Error
|
||||
if err == nil {
|
||||
return p.client.Upsert(ctx, p.tables.KnowledgeItems, id(item.ID), knowledgeItemProjection(item, statusFor(item.Status)))
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) || event.Action != "archive" {
|
||||
return err
|
||||
}
|
||||
var payload struct {
|
||||
ScenarioID uint64 `json:"scenario_id"`
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
if err := json.Unmarshal(event.Payload, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.client.Upsert(ctx, p.tables.KnowledgeItems, id(event.ResourceID), common(event.ResourceID, event.TenantID, "已归档", event.UpdatedAt, map[string]interface{}{
|
||||
"场景来源ID": id(payload.ScenarioID), "知识标识": payload.Key, "名称": payload.Name, "知识类型": payload.Type, "知识状态": payload.Status, "排序": payload.SortOrder,
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *Projector) knowledgeRelation(ctx context.Context, event model.MultiTableOutbox) error {
|
||||
var item model.KnowledgeRelation
|
||||
err := p.db.Where("id = ? AND tenant_id = ?", event.ResourceID, event.TenantID).First(&item).Error
|
||||
if err == nil {
|
||||
return p.client.Upsert(ctx, p.tables.KnowledgeRelations, id(item.ID), knowledgeRelationProjection(item, "正常"))
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) || event.Action != "archive" {
|
||||
return err
|
||||
}
|
||||
var payload struct {
|
||||
ScenarioID uint64 `json:"scenario_id"`
|
||||
FromKnowledge uint64 `json:"from_knowledge_id"`
|
||||
RelationType string `json:"relation_type"`
|
||||
ToKnowledge uint64 `json:"to_knowledge_id"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
if err := json.Unmarshal(event.Payload, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.client.Upsert(ctx, p.tables.KnowledgeRelations, id(event.ResourceID), common(event.ResourceID, event.TenantID, "已归档", event.UpdatedAt, map[string]interface{}{
|
||||
"场景来源ID": id(payload.ScenarioID), "起点知识来源ID": id(payload.FromKnowledge), "关系类型": payload.RelationType, "终点知识来源ID": id(payload.ToKnowledge), "排序": payload.SortOrder,
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *Projector) run(ctx context.Context, event model.MultiTableOutbox) error {
|
||||
var row struct {
|
||||
model.SOPRun
|
||||
SOPName string
|
||||
ScenarioName string
|
||||
}
|
||||
err := p.db.Table("sop_runs r").Select("r.*, s.name AS sop_name, sc.name AS scenario_name").Joins("JOIN sops s ON s.id = r.sop_id").Joins("JOIN scenarios sc ON sc.id = s.scenario_id").Where("r.id = ? AND r.tenant_id = ?", event.ResourceID, event.TenantID).Scan(&row).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if row.ID == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
var answerCount int
|
||||
var answers map[string]interface{}
|
||||
_ = json.Unmarshal(row.Answers, &answers)
|
||||
answerCount = len(answers)
|
||||
return p.client.Upsert(ctx, p.tables.Runs, id(row.ID), common(row.ID, row.TenantID, "正常", row.UpdatedAt, map[string]interface{}{
|
||||
"SOP来源ID": id(row.SOPID), "SOP版本来源ID": id(row.SOPVersionID), "场景名称": row.ScenarioName, "执行人ID": id(row.OperatorID), "执行状态": row.Status, "执行结果": row.Result, "最终结果": jsonText(row.FinalResult), "已采集字段数": answerCount, "开始时间": formatTime(&row.StartedAt), "完成时间": formatTime(row.CompletedAt),
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *Projector) feedback(ctx context.Context, event model.MultiTableOutbox) error {
|
||||
items := make([]model.SOPFeedback, 0)
|
||||
if err := p.db.Where("run_id = ? AND tenant_id = ?", event.ResourceID, event.TenantID).Find(&items).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range items {
|
||||
if err := p.client.Upsert(ctx, p.tables.Feedback, id(item.ID), common(item.ID, item.TenantID, "正常", item.UpdatedAt, map[string]interface{}{
|
||||
"执行来源ID": id(item.RunID), "提交人ID": id(item.UserID), "评分": item.Score, "反馈内容": clip(item.Comment), "提交时间": item.CreatedAt.Format(time.RFC3339),
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func common(sourceID, tenantID uint64, status string, updated time.Time, extra map[string]interface{}) map[string]interface{} {
|
||||
data := map[string]interface{}{"来源ID": id(sourceID), "业务租户ID": tenantID, "同步状态": status, "来源更新时间": updated.Format(time.RFC3339)}
|
||||
for key, value := range extra {
|
||||
data[key] = value
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func scenarioRuleProjection(item model.ScenarioRule, status string) map[string]interface{} {
|
||||
return common(item.ID, item.TenantID, status, item.UpdatedAt, map[string]interface{}{"场景来源ID": id(item.ScenarioID), "规则标识": item.RuleKey, "名称": item.Name, "优先级": item.Priority, "规则状态": item.Status})
|
||||
}
|
||||
|
||||
func knowledgeItemProjection(item model.KnowledgeItem, status string) map[string]interface{} {
|
||||
return common(item.ID, item.TenantID, status, item.UpdatedAt, map[string]interface{}{"场景来源ID": id(item.ScenarioID), "知识标识": item.ItemKey, "名称": item.Name, "知识类型": item.Type, "知识状态": item.Status, "排序": item.SortOrder})
|
||||
}
|
||||
|
||||
func knowledgeRelationProjection(item model.KnowledgeRelation, status string) map[string]interface{} {
|
||||
return common(item.ID, item.TenantID, status, item.UpdatedAt, map[string]interface{}{"场景来源ID": id(item.ScenarioID), "起点知识来源ID": id(item.FromKnowledgeID), "关系类型": item.RelationType, "终点知识来源ID": id(item.ToKnowledgeID), "排序": item.SortOrder})
|
||||
}
|
||||
|
||||
func id(value uint64) string { return strconv.FormatUint(value, 10) }
|
||||
func formatTime(value *time.Time) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return value.Format(time.RFC3339)
|
||||
}
|
||||
func statusFor(value string) string {
|
||||
if value == "archived" {
|
||||
return "已归档"
|
||||
}
|
||||
if value == "offline" {
|
||||
return "已下线"
|
||||
}
|
||||
return "正常"
|
||||
}
|
||||
func jsonText(value []byte) string { return clip(string(value)) }
|
||||
func stringValue(value interface{}) string { result, _ := value.(string); return result }
|
||||
func clip(value string) string {
|
||||
if len(value) > 4000 {
|
||||
return value[:4000]
|
||||
}
|
||||
return value
|
||||
}
|
||||
func yesNo(value bool) string {
|
||||
if value {
|
||||
return "是"
|
||||
}
|
||||
return "否"
|
||||
}
|
||||
Reference in New Issue
Block a user