2c7265efc4
build / build (push) Successful in 58s
主人要求:去掉入口,统一成 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。
80 lines
3.0 KiB
Go
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,
|
|
})
|
|
}
|