package httpapi import ( "context" "errors" "net/http" "time" "knowledge-governance/internal/platform/postgres" ) // 「去掉来源」= 归档下架(不是物理删除)。管理员专用、幂等。 // // 归档后来源从数据源配置、来源与目录、计划刷新与手动扫描里全部消失,但目录快照、 // 发布记录与审计数据原样保留 —— 恢复用 /restore,或让 dws 重新登记同一空间 // (钉钉空间登记是确定性 id + ON CONFLICT,会原地复活同一行,不产生重复)。 // // 为什么不给物理删除:source_items / source_catalog_snapshots / test_publications / // release_executions 都以 ON DELETE RESTRICT 引用来源连接。硬删要么失败,要么 // 连带毁掉发布与审计记录。 func archiveSourceConnection(w http.ResponseWriter, r *http.Request, config Config) { mutateSourceConnectionArchive(w, r, config, true) } func restoreSourceConnection(w http.ResponseWriter, r *http.Request, config Config) { mutateSourceConnectionArchive(w, r, config, false) } // sourceConnectionArchiver 是本路由需要的最小能力面:归档与恢复各一个写操作。 type sourceConnectionArchiver interface { ArchiveSourceConnection(ctx context.Context, connectionID, actor string, now time.Time) error RestoreSourceConnection(ctx context.Context, connectionID, actor string, now time.Time) error } func mutateSourceConnectionArchive(w http.ResponseWriter, r *http.Request, config Config, archive bool) { if _, ok := requireSessionIdentity(w, r); !ok { return } // 写意图与幂等键与其它来源设置写操作同规格:没有意图头就不写。 wantIntent := "source-archive" if !archive { wantIntent = "source-restore" } if r.Header.Get("X-Governance-Action") != wantIntent { writeJSON(w, http.StatusForbidden, map[string]string{"error": "source archive write intent is required"}) return } operationID, ok := requireOperationID(w, r) if !ok { return } connectionID := r.PathValue("id") if !validCatalogID(connectionID) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid source connection"}) return } store, ok := config.Store.(sourceConnectionArchiver) if !ok { writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "source archive is not wired"}) return } var err error if archive { err = store.ArchiveSourceConnection(r.Context(), connectionID, actorFor(r), config.Now().UTC()) } else { err = store.RestoreSourceConnection(r.Context(), connectionID, actorFor(r), config.Now().UTC()) } switch { case errors.Is(err, postgres.ErrSourceConnectionNotFound): writeJSON(w, http.StatusNotFound, map[string]string{"error": "source connection not found"}) return case err != nil: writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "could not change source archive state"}) return } writeJSON(w, http.StatusOK, map[string]any{ "id": connectionID, "archived": archive, "operation_id": operationID, }) }