package service import ( "fmt" "strings" "git.gozeros.com/wsk/eightbf-analysis-go/internal/domain" ) const ( RoleAdmin = "admin" RoleTaskOwner = "task_owner" RolePositionOwner = "position_owner" RoleViewer = "viewer" ) // Caller 当前请求调用方(来自 X-Account-Id / X-Analysis-Role)。 type Caller struct { AccountID string Role string } func NewCaller(accountID, role string) Caller { r := strings.TrimSpace(role) if r == "" { r = RoleViewer } return Caller{AccountID: strings.TrimSpace(accountID), Role: r} } // AccessService 岗位账号限制与对话投影。 type AccessService struct { AuthEnabled bool } // CanMutate 谁可以续跑、改计划、注入。 // // 鉴权关闭时一律放行(联调)。打开后:没账号拒绝;管理员、任务责任人角色、作业拥有者、 // access.taskOwnerAccountIds / adminAccountIds 里的账号可以改。 // 岗位责任人只有绑了席位才能改本场,取消权另见 CanCancel。 func (a *AccessService) CanMutate(job *domain.AnalysisJob, c Caller) bool { if !a.AuthEnabled { return true } if c.AccountID == "" { return false } if c.Role == RoleAdmin || c.Role == RoleTaskOwner { return true } if c.AccountID == strings.TrimSpace(job.OwnerAccountID) { return true } if a.isListedTaskOwner(job, c.AccountID) { return true } return c.Role == RolePositionOwner && len(a.boundSeats(job, c.AccountID)) > 0 } // CanCancel 谁可以取消。岗位责任人默认不能取消,除非他同时是作业拥有者或名单里的任务责任人。 // 这条比 CanMutate 更严:席位上的人可以插话,但不能把整场停掉。 func (a *AccessService) CanCancel(job *domain.AnalysisJob, c Caller) bool { if c.Role == RolePositionOwner { if c.AccountID == strings.TrimSpace(job.OwnerAccountID) || a.isListedTaskOwner(job, c.AccountID) { return true } return false } if !a.AuthEnabled { return true } if c.AccountID == "" { return false } if c.Role == RoleAdmin || c.Role == RoleTaskOwner { return true } if c.AccountID == strings.TrimSpace(job.OwnerAccountID) { return true } return a.isListedTaskOwner(job, c.AccountID) } // CanRead 谁可以看。能改或能取消的人可以看;只绑了席位、没有写权限的账号也可以看(随后会被裁剪)。 func (a *AccessService) CanRead(job *domain.AnalysisJob, c Caller) bool { if !a.AuthEnabled { return true } if c.AccountID == "" { return false } if a.CanMutate(job, c) || a.CanCancel(job, c) { return true } return len(a.boundSeats(job, c.AccountID)) > 0 } // ProjectForCaller 按角色裁剪对话后返回(责任人全量;岗位只见本席+总控/系统)。 func (a *AccessService) ProjectForCaller(job *domain.AnalysisJob, c Caller) *domain.AnalysisJob { if job == nil { return nil } if c.AccountID == "" { return job } if c.Role == RoleAdmin || c.Role == RoleTaskOwner || c.AccountID == strings.TrimSpace(job.OwnerAccountID) || a.isListedTaskOwner(job, c.AccountID) { return job } if c.Role != RolePositionOwner && !a.AuthEnabled { return job } seats := a.boundSeats(job, c.AccountID) copyJob := *job if len(seats) == 0 { var filtered []map[string]interface{} for _, d := range job.Dialogues { role := asStr(d["role"]) if role == "system" || role == "user" { filtered = append(filtered, d) } } copyJob.Dialogues = filtered if job.Result != nil { r := cloneMap(job.Result) delete(r, "seatDeliveries") copyJob.Result = r } return ©Job } var filtered []map[string]interface{} for _, d := range job.Dialogues { role := asStr(d["role"]) agentID := asStr(d["agentId"]) if role == "system" || role == "user" || role == "leader" { filtered = append(filtered, d) } else if seats[agentID] { filtered = append(filtered, d) } } copyJob.Dialogues = filtered if job.Result != nil { r := cloneMap(job.Result) if raw, ok := r["seatDeliveries"].([]interface{}); ok { var kept []interface{} for _, o := range raw { m, ok := o.(map[string]interface{}) if !ok { continue } if seats[asStr(m["agentId"])] { kept = append(kept, m) } } r["seatDeliveries"] = kept } copyJob.Result = r } return ©Job } func (a *AccessService) boundSeats(job *domain.AnalysisJob, accountID string) map[string]bool { out := map[string]bool{} if job == nil || accountID == "" || job.Access == nil { return out } raw, ok := job.Access["seatAccounts"] if !ok { return out } m, ok := raw.(map[string]interface{}) if !ok { return out } for k, v := range m { if accountInList(v, accountID) { out[k] = true } } return out } func (a *AccessService) isListedTaskOwner(job *domain.AnalysisJob, accountID string) bool { if job == nil || job.Access == nil { return false } return accountInList(job.Access["taskOwnerAccountIds"], accountID) || accountInList(job.Access["adminAccountIds"], accountID) } func accountInList(raw interface{}, accountID string) bool { if raw == nil || accountID == "" { return false } switch t := raw.(type) { case []interface{}: for _, o := range t { if strings.TrimSpace(asStr(o)) == accountID { return true } } return false default: return strings.TrimSpace(asStr(raw)) == accountID } } func cloneMap(m map[string]interface{}) map[string]interface{} { out := make(map[string]interface{}, len(m)) for k, v := range m { out[k] = v } return out } func asStr(v interface{}) string { if v == nil { return "" } if s, ok := v.(string); ok { return s } return strings.TrimSpace(fmt.Sprint(v)) }