3e7fcce0b1
build / build (push) Successful in 1m11s
新增迁移50与每5分钟/手动持久同步,API只读本地;同步失败保留、权限阻断、租约与质量失效原子激活。 保留全量稳定分页/固定列,已存质量批量读取,元数据不反复解压长答案;详情安全Markdown与原文。 已运行整仓 make verify、源与副本隔离PG -race、真实只读源及隔离浏览器。 实际同步2525条;固定窗口640条本地中位1.46秒,对照直接源33.10秒。 停worker、缺源凭据、重启API后仍可查询;未更新8088,未修改生产原问答。
80 lines
2.3 KiB
Go
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})
|
|
}
|