Files
knowledge-governance/internal/qaopsorg/attribution.go
T
姚章浩 533db535f1
build / build (push) Successful in 1m12s
feat(qaops): 接入真实模型补缺任务与总表看板
按已确认需求提供独立安全模型配置、真实探测、正式字典字段评估与原问题译中,持久批次支持只补空白、暂停恢复、有限重试、幂等和逐字段依据。覆盖固定规则、本地语言、组织关联与主表有效看板共用治理侧读模型,保留源生产和冻结历史。

验证:完整 make verify 退出0,702项Go与329项前端通过;默认118项外部/隔离集成明确跳过,另跑完整镜像及组织隔离PG竞态。12并发、输入漂移、过期租约、暂停/恢复及有限重试通过。用户授权的百炼 deepseek-v4.1-flash 完成真实后台与浏览器评估/翻译;552行、16列、11块看板及完整PNG、独立配置页测试/保存、重开恢复和零JS异常通过。仅非业务样本外发,自动处理关闭,原payload不变。

上线前治理库已做导出快照原生备份并真实恢复,60表行数与17个QA表内容哈希一致;不修改DobotAI生产或隧道。本提交时尚未部署,线上验收须独立完成。
2026-10-19 14:29:49 +08:00

186 lines
4.9 KiB
Go

package qaopsorg
import (
"context"
"encoding/json"
"github.com/jackc/pgx/v5"
"sort"
"strings"
)
type PersonAttribution struct {
Name string `json:"name"`
DepartmentIDs []string `json:"-"`
State string `json:"state"`
PersonID string `json:"-"`
Departments []string `json:"departments"`
Region string `json:"region"`
}
type Attribution struct {
Metadata State
people map[string]PersonAttribution
unions map[string]string
}
func (p *Attribution) Members() []PersonAttribution {
result := []PersonAttribution{}
if p == nil {
return result
}
for _, person := range p.people {
result = append(result, person)
}
sort.Slice(result, func(i, j int) bool { return result[i].PersonID < result[j].PersonID })
return result
}
func BuildAttribution(state State, directory Directory) (*Attribution, error) {
p := &Attribution{Metadata: state, people: map[string]PersonAttribution{}, unions: map[string]string{}}
if state.State == "empty" {
return p, nil
}
if state.State != "ready" || state.DirectoryID == "" {
return nil, ErrUnavailable
}
view, err := PreviewRegions(directory, state.Rules, state.RegionDefinitions())
if err != nil {
return nil, ErrUnavailable
}
p.Metadata.View = view
departments := map[string]DepartmentView{}
for _, d := range view.Departments {
departments[d.ID] = d
}
for _, person := range directory.People {
paths, regions := map[string]bool{}, map[string]bool{}
for _, id := range person.DepartmentIDs {
d := departments[id]
paths[d.Path] = true
if d.Region != "" {
regions[d.Region] = true
}
}
item := PersonAttribution{Name: person.Name, DepartmentIDs: append([]string{}, person.DepartmentIDs...), State: "unassigned", PersonID: person.ID, Departments: []string{}}
for path := range paths {
item.Departments = append(item.Departments, path)
}
sort.Strings(item.Departments)
if len(regions) == 1 {
item.State = "matched"
for region := range regions {
item.Region = state.RegionLabel(region)
}
} else if len(regions) > 1 {
item.State = "cross_region"
}
p.people[person.ID] = item
if person.UnionID != "" {
if !validID(person.UnionID) {
return nil, ErrUnavailable
}
if _, ok := p.unions[person.UnionID]; ok {
p.unions[person.UnionID] = ""
} else {
p.unions[person.UnionID] = person.ID
}
}
}
return p, nil
}
func (p *Attribution) Resolve(staff, union string, evidence ...string) PersonAttribution {
none := PersonAttribution{State: "unmatched", Departments: []string{}}
if p == nil || p.Metadata.State != "ready" {
none.State = "directory_missing"
return none
}
if len(evidence) >= 2 {
asker, raw := strings.TrimSpace(evidence[0]), strings.TrimSpace(evidence[1])
if strings.HasPrefix(raw, "{") || strings.HasPrefix(raw, "[") {
var legacy map[string]json.RawMessage
if json.Unmarshal([]byte(raw), &legacy) != nil || legacy == nil {
none.State = "identity_conflict"
return none
}
read := func(key string) (string, bool) {
value, present := legacy[key]
if !present {
return "", true
}
var text string
if json.Unmarshal(value, &text) != nil {
return "", false
}
return strings.TrimSpace(text), true
}
legacyStaff, validStaff := read("staffId")
legacyUnion, validUnion := read("unionId")
if !validStaff || !validUnion || staff != "" && legacyStaff != "" && staff != legacyStaff || union != "" && legacyUnion != "" && union != legacyUnion {
none.State = "identity_conflict"
return none
}
if staff == "" {
staff = legacyStaff
}
if union == "" {
union = legacyUnion
}
}
if staff == "" && union == "" && asker != "" {
var match *PersonAttribution
for _, person := range p.people {
if strings.TrimSpace(person.Name) == asker {
if match != nil {
none.State = "identity_conflict"
return none
}
copy := person
match = &copy
}
}
if match != nil {
return *match
}
}
}
if staff == "" && union == "" {
none.State = "no_identity"
return none
}
byUnion, unionKnown := p.unions[union]
if person, ok := p.people[staff]; ok {
if unionKnown && (byUnion == "" || byUnion != staff) {
none.State = "identity_conflict"
return none
}
return person
}
if unionKnown {
if byUnion == "" {
none.State = "identity_conflict"
return none
}
return p.people[byUnion]
}
return none
}
func (s *Store) GetAttributionInTx(ctx context.Context, tx pgx.Tx) (*Attribution, error) {
if !s.available() {
return nil, ErrUnavailable
}
state, directory, err := s.read(ctx, tx, false)
if err != nil {
return nil, err
}
return BuildAttribution(state, directory)
}
func (s *Store) GetAttribution(ctx context.Context) (*Attribution, error) {
if !s.available() {
return nil, ErrUnavailable
}
state, directory, err := s.read(ctx, s.pool, false)
if err != nil {
return nil, err
}
return BuildAttribution(state, directory)
}