Files
knowledge-governance/internal/httpapi/qa_mirror_sync.go
姚章浩 3e7fcce0b1
build / build (push) Successful in 1m11s
feat(qa-ops): 持久本地问答副本与安全答案预览
新增迁移50与每5分钟/手动持久同步,API只读本地;同步失败保留、权限阻断、租约与质量失效原子激活。
保留全量稳定分页/固定列,已存质量批量读取,元数据不反复解压长答案;详情安全Markdown与原文。

已运行整仓 make verify、源与副本隔离PG -race、真实只读源及隔离浏览器。
实际同步2525条;固定窗口640条本地中位1.46秒,对照直接源33.10秒。
停worker、缺源凭据、重启API后仍可查询;未更新8088,未修改生产原问答。
2026-10-18 19:10:47 +08:00

80 lines
2.3 KiB
Go

package httpapi
import (
"context"
"encoding/json"
"errors"
"io"
"knowledge-governance/internal/qaopsmirror"
"net/http"
"strings"
)
type qaMirrorSyncStore interface {
GetSyncStatus(context.Context) (qaopsmirror.Status, error)
EnqueueSync(context.Context, string, string) (qaopsmirror.SyncReceipt, bool, error)
}
func qaRecordsSync(w http.ResponseWriter, r *http.Request, c Config) {
if !authorizeQAOps(w, r) {
return
}
if r.URL.RawQuery != "" {
writeJSON(w, 400, map[string]string{"error": "同步入口不接受查询参数"})
return
}
store, ok := c.QAOps.(qaMirrorSyncStore)
if !ok || !c.QAOpsEnabled {
writeJSON(w, 503, map[string]string{"error": "本地问答同步未接线,未创建任务"})
return
}
if r.Method == http.MethodGet {
state, e := store.GetSyncStatus(r.Context())
if e != nil {
qaOpsError(w, e)
return
}
writeJSON(w, 200, map[string]any{"data": state})
return
}
if !authorizeQAIntent(w, r, "qa-records-sync") {
return
}
key := r.Header.Get("Idempotency-Key")
if key == "" || len(key) > 128 || strings.TrimSpace(key) != key {
writeJSON(w, 400, map[string]string{"error": "同步请求需要有效 Idempotency-Key,重试须复用原标识"})
return
}
var body *struct{}
d := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1024))
d.DisallowUnknownFields()
if d.Decode(&body) != nil || body == nil || d.Decode(&struct{}{}) != io.EOF {
writeJSON(w, 400, map[string]string{"error": "同步请求体必须是空对象"})
return
}
identity, _ := sessionFrom(r.Context())
receipt, replayed, e := store.EnqueueSync(r.Context(), key, identity.Username)
if e != nil {
status := 503
message := "同步未成功入队,已有本地数据保持不变"
if errors.Is(e, qaopsmirror.ErrInvalid) {
status = 400
message = "同步请求标识无效"
}
if errors.Is(e, qaopsmirror.ErrConflict) {
status = 409
message = "同步标识已用于另一操作,请重新发起"
}
if errors.Is(e, qaopsmirror.ErrWorkerUnavailable) {
message = "问答同步执行器未在线,未创建任务"
}
writeJSON(w, status, map[string]string{"error": message})
return
}
operation := "created"
if replayed {
operation = "replayed"
}
writeJSON(w, 202, map[string]any{"data": receipt, "operation": operation})
}