87f97a2f9b
build / build (push) Successful in 1m7s
详情使用共享主表字典,稀疏修改所选字段并保留原始问答及旧复核审计。 月周统计提供文本和下拉双入口;每租户每周一份持久草稿,按技能六节双语模板真实预填。 加入幂等自动保存、冲突保留、未知响应重试、切周及离开保护和schema58非破坏降级保护。 验证:完整make verify、隔离PostgreSQL全mirror竞态回归、真实恢复库浏览器验收通过。 浏览器实测字段精确读回、月周双输入、模板预填、MD/CSV、自动保存、冲突、失败重试与API重启恢复。 未连接或改写DobotAI生产源库,未运行付费模型;提交时尚未部署。
152 lines
7.2 KiB
Go
152 lines
7.2 KiB
Go
package qaopsmirror
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"github.com/jackc/pgx/v5"
|
|
"knowledge-governance/internal/dobotaiops"
|
|
"knowledge-governance/internal/qaopsfields"
|
|
"knowledge-governance/internal/qaopsquality"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type MasterFieldPatch struct {
|
|
InputHash string `json:"input_hash"`
|
|
InputEpoch int64 `json:"input_epoch"`
|
|
BaseRevision int64 `json:"base_revision"`
|
|
BaseReviewRevision int64 `json:"base_review_revision"`
|
|
Nonce string `json:"nonce"`
|
|
Fields map[string]string `json:"fields"`
|
|
}
|
|
type MasterFieldReceipt struct {
|
|
SavedRevision int64 `json:"saved_revision"`
|
|
Replayed bool `json:"replayed"`
|
|
}
|
|
|
|
var inlineHash = regexp.MustCompile(`^[a-f0-9]{64}$`)
|
|
var inlineNonce = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.:-]{15,127}$`)
|
|
|
|
func validInlineID(value string) bool {
|
|
return value != "" && len(value) <= 256 && utf8.ValidString(value) && !strings.ContainsAny(value, "\x00\x1f")
|
|
}
|
|
func (s *Store) PatchMasterFields(ctx context.Context, id string, q MasterFieldPatch, actor string) (MasterFieldReceipt, error) {
|
|
if !s.valid() {
|
|
return MasterFieldReceipt{}, dobotaiops.ErrUnavailable
|
|
}
|
|
if !validInlineID(id) || !validInlineID(actor) || !inlineHash.MatchString(q.InputHash) || !inlineNonce.MatchString(q.Nonce) || q.InputEpoch < 1 || q.BaseRevision < 0 || q.BaseReviewRevision < 0 || len(q.Fields) < 1 || len(q.Fields) > 5 {
|
|
return MasterFieldReceipt{}, dobotaiops.ErrInvalidQuery
|
|
}
|
|
for field, value := range q.Fields {
|
|
if !qaopsfields.ValidEditableValue(field, value) {
|
|
return MasterFieldReceipt{}, dobotaiops.ErrInvalidQuery
|
|
}
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, 8*time.Second)
|
|
defer cancel()
|
|
tx, err := s.pool.Begin(ctx)
|
|
if err != nil {
|
|
return MasterFieldReceipt{}, dobotaiops.ErrUnavailable
|
|
}
|
|
defer tx.Rollback(context.Background())
|
|
if _, err = tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1||chr(31)||$2,258))`, s.tenant, id); err != nil {
|
|
return MasterFieldReceipt{}, dobotaiops.ErrUnavailable
|
|
}
|
|
payload, _ := json.Marshal(q)
|
|
sum := sha256.Sum256(payload)
|
|
requestHash := hex.EncodeToString(sum[:])
|
|
var savedRevision int64
|
|
var savedHash, savedActor string
|
|
err = tx.QueryRow(ctx, `SELECT revision,request_hash,actor FROM qa_ops_master_field_edits WHERE tenant_key=$1 AND record_id=$2 AND nonce=$3`, s.tenant, id, q.Nonce).Scan(&savedRevision, &savedHash, &savedActor)
|
|
if err == nil {
|
|
if savedHash != requestHash || savedActor != actor {
|
|
return MasterFieldReceipt{}, qaopsquality.ErrConflict
|
|
}
|
|
return MasterFieldReceipt{SavedRevision: savedRevision, Replayed: true}, nil
|
|
}
|
|
if !errors.Is(err, pgx.ErrNoRows) {
|
|
return MasterFieldReceipt{}, dobotaiops.ErrUnavailable
|
|
}
|
|
var hash string
|
|
var reviewRevision, epoch int64
|
|
err = tx.QueryRow(ctx, `SELECT input_hash,review_revision,input_epoch FROM qa_ops_quality_records WHERE tenant_key=$1 AND record_id=$2 FOR UPDATE`, s.tenant+"::master", id).Scan(&hash, &reviewRevision, &epoch)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return MasterFieldReceipt{}, dobotaiops.ErrRecordNotFound
|
|
}
|
|
if err != nil {
|
|
return MasterFieldReceipt{}, dobotaiops.ErrUnavailable
|
|
}
|
|
if hash != q.InputHash || epoch != q.InputEpoch || reviewRevision != q.BaseReviewRevision {
|
|
return MasterFieldReceipt{}, qaopsquality.ErrConflict
|
|
}
|
|
var currentRevision, previousReview int64
|
|
var previousHash string
|
|
var raw []byte
|
|
overrides := map[string]string{}
|
|
err = tx.QueryRow(ctx, `SELECT revision,input_hash,review_revision,fields FROM qa_ops_master_field_overrides WHERE tenant_key=$1 AND record_id=$2`, s.tenant, id).Scan(¤tRevision, &previousHash, &previousReview, &raw)
|
|
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
|
return MasterFieldReceipt{}, dobotaiops.ErrUnavailable
|
|
}
|
|
if currentRevision != q.BaseRevision {
|
|
return MasterFieldReceipt{}, qaopsquality.ErrConflict
|
|
}
|
|
if previousHash == hash && previousReview == reviewRevision && json.Unmarshal(raw, &overrides) != nil {
|
|
return MasterFieldReceipt{}, dobotaiops.ErrUnavailable
|
|
}
|
|
var masterHash string
|
|
var fieldsRaw, coverageRaw, editsRaw []byte
|
|
err = tx.QueryRow(ctx, `SELECT quality_hash,effective_fields,coverage_result,inline_fields FROM qa_ops_master_read_model WHERE tenant_key=$1 AND id=$2`, s.tenant, id).Scan(&masterHash, &fieldsRaw, &coverageRaw, &editsRaw)
|
|
if err != nil {
|
|
return MasterFieldReceipt{}, dobotaiops.ErrUnavailable
|
|
}
|
|
if masterHash != q.InputHash {
|
|
return MasterFieldReceipt{}, qaopsquality.ErrConflict
|
|
}
|
|
var fields, edits map[string]string
|
|
var coverage dobotaiops.CoverageCheck
|
|
if json.Unmarshal(fieldsRaw, &fields) != nil || json.Unmarshal(editsRaw, &edits) != nil || json.Unmarshal(coverageRaw, &coverage) != nil {
|
|
return MasterFieldReceipt{}, dobotaiops.ErrUnavailable
|
|
}
|
|
views, err := s.MasterQualityStore().ReadViewsInTx(ctx, tx, []dobotaiops.QualityKey{{ID: id, InputHash: masterHash}})
|
|
if err != nil {
|
|
return MasterFieldReceipt{}, err
|
|
}
|
|
current := qaopsquality.ProjectMasterWithEdits(views[id], fields, coverage, edits).Effective
|
|
beforeAll := map[string]string{"coverage": map[string]string{"covered": "是", "uncovered": "否", "partial": "部分覆盖", "pending": "待确认", "excluded": "否但不计入"}[current.Coverage], "is_correct": map[string]string{"correct": "是", "incorrect": "否", "pending": "待确认"}[current.Correctness], "product_category": current.ProductCategory, "problem_type": current.ProblemType, "root_cause": current.RootCause}
|
|
correctness := beforeAll["is_correct"]
|
|
if value, ok := q.Fields["is_correct"]; ok {
|
|
correctness = value
|
|
}
|
|
if correctness == "是" && q.Fields["root_cause"] != "" {
|
|
return MasterFieldReceipt{}, dobotaiops.ErrInvalidQuery
|
|
}
|
|
before := map[string]string{}
|
|
for key, value := range q.Fields {
|
|
before[key] = beforeAll[key]
|
|
overrides[key] = value
|
|
}
|
|
encoded, _ := json.Marshal(overrides)
|
|
changed, _ := json.Marshal(q.Fields)
|
|
previous, _ := json.Marshal(before)
|
|
revision := currentRevision + 1
|
|
if _, err = tx.Exec(ctx, `INSERT INTO qa_ops_master_field_overrides(tenant_key,record_id,revision,input_hash,review_revision,fields) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(tenant_key,record_id) DO UPDATE SET revision=EXCLUDED.revision,input_hash=EXCLUDED.input_hash,review_revision=EXCLUDED.review_revision,fields=EXCLUDED.fields,updated_at=now()`, s.tenant, id, revision, hash, reviewRevision, encoded); err != nil {
|
|
return MasterFieldReceipt{}, dobotaiops.ErrUnavailable
|
|
}
|
|
if _, err = tx.Exec(ctx, `INSERT INTO qa_ops_master_field_edits(tenant_key,record_id,revision,input_hash,review_revision,changed_fields,before_fields,fields,actor,nonce,request_hash) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)`, s.tenant, id, revision, hash, reviewRevision, changed, previous, encoded, actor, q.Nonce, requestHash); err != nil {
|
|
return MasterFieldReceipt{}, dobotaiops.ErrUnavailable
|
|
}
|
|
tag, err := tx.Exec(ctx, `UPDATE qa_ops_master_records SET row_revision=row_revision+1 WHERE tenant_key=$1 AND id=$2 AND quality_hash=$3`, s.tenant, id, hash)
|
|
if err != nil || tag.RowsAffected() != 1 {
|
|
return MasterFieldReceipt{}, qaopsquality.ErrConflict
|
|
}
|
|
if err = tx.Commit(ctx); err != nil {
|
|
return MasterFieldReceipt{}, dobotaiops.ErrUnavailable
|
|
}
|
|
return MasterFieldReceipt{SavedRevision: revision}, nil
|
|
}
|