533db535f1
build / build (push) Successful in 1m12s
按已确认需求提供独立安全模型配置、真实探测、正式字典字段评估与原问题译中,持久批次支持只补空白、暂停恢复、有限重试、幂等和逐字段依据。覆盖固定规则、本地语言、组织关联与主表有效看板共用治理侧读模型,保留源生产和冻结历史。 验证:完整 make verify 退出0,702项Go与329项前端通过;默认118项外部/隔离集成明确跳过,另跑完整镜像及组织隔离PG竞态。12并发、输入漂移、过期租约、暂停/恢复及有限重试通过。用户授权的百炼 deepseek-v4.1-flash 完成真实后台与浏览器评估/翻译;552行、16列、11块看板及完整PNG、独立配置页测试/保存、重开恢复和零JS异常通过。仅非业务样本外发,自动处理关闭,原payload不变。 上线前治理库已做导出快照原生备份并真实恢复,60表行数与17个QA表内容哈希一致;不修改DobotAI生产或隧道。本提交时尚未部署,线上验收须独立完成。
186 lines
4.9 KiB
Go
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 = ©
|
|
}
|
|
}
|
|
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)
|
|
}
|