Files
knowledge-governance/internal/httpapi/source_connection_archive.go
T
姚章浩 2c7265efc4
build / build (push) Successful in 58s
feat(sources): 数据源配置收敛成 dws 导入 + PP 导入;「去掉来源」= 归档下架(不删数据)
主人要求:去掉入口,统一成 dws 导入与 PP 导入;其他来源连数据一起下架,之后用 dws 重新获取。

界面(web):
- 「数据源配置」只剩两块:**dws 导入(钉钉)**(按当前钉钉账号列出可访问的知识库与钉盘空间)
  与 **PP 导入**(PP 资料下载中心)。上一步加的「显示 AList/钉钉知识库来源」折叠开关删除。
- 来源卡片新增管理员可见的「去掉来源」(带确认弹窗,说明是归档下架、数据保留可恢复)。
- 侧栏文案改成"只留两个入口";发现面板标题改为「dws 导入(钉钉)」。

后端(归档语义,可逆):
- 迁移 000047:source_connections 增 archived_at / archived_by + 只覆盖未归档行的部分索引。
- store:ArchiveSourceConnection / RestoreSourceConnection(幂等;归档同时停用并清空
  next_refresh_at,避免被手工改回后立刻带着旧调度跑)。列表、目录、计划刷新(claimDue)、
  手动扫描(triggerBlock/inspect)四类查询一律排除归档行。
- 注册路径(dws 重新获取):RegisterDingtalkSpace 的 ON CONFLICT 清掉归档标记 ——
  重新登记同一个空间**原地复活同一行**,不产生重复,历史快照仍属于同一行。
- HTTP:POST /api/v1/source-connections/{id}/archive|restore(管理员专属,写意图头
  source-archive / source-restore + Idempotency-Key;404 区分"不存在"与"已是目标状态");
  openapi 同步两条契约。

验证:
- 集成测试(真 postgres):归档从列表消失但 source_items 一行不少、重复归档不漂移归档时间与人、
  未知 ID 报 ErrSourceConnectionNotFound、归档来源不能再被扫描、恢复后回到列表但保持停用;
  dws 重新登记同一空间复活同一行。
- httpapi:写意图/幂等键门禁、200/403/404/500、错误体不回显内部细节、classifyRole 管理员专属。
- 迁移护栏:000047 结构 + 四类 SQL 必须含 archived_at is null(漏一条就是入口没去掉)。
- make verify 15/15。
2026-10-14 12:36:32 +08:00

80 lines
3.0 KiB
Go

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,
})
}