package repository import ( "errors" "strings" "time" "hyapp-admin-server/internal/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) func (s *Store) ListRoleDataScopes(roleID uint) ([]model.DataScope, error) { var scopes []model.DataScope err := s.db.Where("role_id = ?", roleID).Order("scope_type ASC, scope_value ASC").Find(&scopes).Error return scopes, err } func (s *Store) DataAccessForUser(userID uint) (DataAccess, error) { if userID == 0 { return DataAccess{All: true}, nil } var user model.User if err := s.db.Preload("Roles").First(&user, userID).Error; err != nil { return DataAccess{}, err } roleIDs := make([]uint, 0, len(user.Roles)) for _, role := range user.Roles { roleIDs = append(roleIDs, role.ID) if role.Code == "platform-admin" { return DataAccess{UserID: userID, All: true}, nil } } if len(roleIDs) == 0 { return DataAccess{UserID: userID, All: true}, nil } var scopes []model.DataScope if err := s.db.Where("role_id IN ?", roleIDs).Find(&scopes).Error; err != nil { return DataAccess{}, err } if len(scopes) == 0 { return DataAccess{UserID: userID, All: true}, nil } access := DataAccess{UserID: userID, Restricted: true} teamSet := map[string]struct{}{} for _, scope := range scopes { scopeType := strings.TrimSpace(scope.ScopeType) scopeValue := strings.TrimSpace(scope.ScopeValue) switch scopeType { case "all": return DataAccess{UserID: userID, All: true}, nil case "self": access.SelfOnly = true case "team": if scopeValue == "self" && strings.TrimSpace(user.Team) != "" { teamSet[user.Team] = struct{}{} continue } if scopeValue != "" { teamSet[scopeValue] = struct{}{} } } } for team := range teamSet { access.Teams = append(access.Teams, team) } return access, nil } func (s *Store) ReplaceRoleDataScopes(roleID uint, scopes []model.DataScope) error { if roleID == 0 { return errors.New("role id is required") } for i := range scopes { scopes[i].RoleID = roleID scopes[i].ScopeType = strings.TrimSpace(scopes[i].ScopeType) scopes[i].ScopeValue = strings.TrimSpace(scopes[i].ScopeValue) if scopes[i].ScopeType == "" || scopes[i].ScopeValue == "" { return errors.New("scope type and value are required") } } return s.db.Transaction(func(tx *gorm.DB) error { var role model.Role if err := tx.First(&role, roleID).Error; err != nil { return err } if err := tx.Where("role_id = ?", roleID).Delete(&model.DataScope{}).Error; err != nil { return err } if len(scopes) == 0 { return nil } return tx.Create(&scopes).Error }) } func (s *Store) CreateJob(job *model.AdminJob) error { if strings.TrimSpace(job.Type) == "" { return errors.New("job type is required") } job.Type = strings.TrimSpace(job.Type) job.Status = model.JobStatusPending if job.MaxAttempts <= 0 { job.MaxAttempts = 3 } return s.db.Create(job).Error } func (s *Store) ListJobs(options ListOptions) ([]model.AdminJob, int64, error) { page, pageSize := normalizePage(options.Page, options.PageSize) query := s.db.Model(&model.AdminJob{}) if keyword := strings.TrimSpace(options.Keyword); keyword != "" { like := "%" + keyword + "%" query = query.Where("type LIKE ? OR created_by_name LIKE ? OR error LIKE ?", like, like, like) } if status := strings.TrimSpace(options.Status); status != "" && status != "all" { query = query.Where("status = ?", status) } var total int64 if err := query.Count(&total).Error; err != nil { return nil, 0, err } var jobs []model.AdminJob err := query.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&jobs).Error return jobs, total, err } func (s *Store) LeaseNextJob(workerID string, lease time.Duration, maxAttempts int) (*model.AdminJob, error) { if strings.TrimSpace(workerID) == "" { return nil, errors.New("worker id is required") } if lease <= 0 { return nil, errors.New("lease must be greater than 0") } if maxAttempts <= 0 { maxAttempts = 3 } now := time.Now() var leased model.AdminJob err := s.db.Transaction(func(tx *gorm.DB) error { var job model.AdminJob err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("(status = ? OR (status = ? AND locked_until IS NOT NULL AND locked_until < ?)) AND attempt < max_attempts", model.JobStatusPending, model.JobStatusRunning, now). Order("created_at ASC, id ASC"). First(&job).Error if err != nil { return err } if job.MaxAttempts <= 0 { job.MaxAttempts = maxAttempts } updates := map[string]interface{}{ "status": model.JobStatusRunning, "attempt": job.Attempt + 1, "max_attempts": job.MaxAttempts, "locked_by": workerID, "locked_until": now.Add(lease), "started_at": firstTime(job.StartedAt, now), "error": "", } if err := tx.Model(&model.AdminJob{}).Where("id = ?", job.ID).Updates(updates).Error; err != nil { return err } if err := tx.First(&leased, job.ID).Error; err != nil { return err } return nil }) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, gorm.ErrRecordNotFound } if err != nil { return nil, err } return &leased, nil } func (s *Store) CompleteJobWithArtifact(id uint, resultJSON string, artifactPath string) error { now := time.Now() return s.db.Model(&model.AdminJob{}).Where("id = ?", id).Updates(map[string]interface{}{ "status": model.JobStatusSucceeded, "result_json": resultJSON, "artifact_path": artifactPath, "locked_by": "", "locked_until": nil, "finished_at": &now, }).Error } func (s *Store) FailJobAttempt(id uint, message string) error { var job model.AdminJob if err := s.db.First(&job, id).Error; err != nil { return err } now := time.Now() status := model.JobStatusPending finishedAt := interface{}(nil) if job.Attempt >= job.MaxAttempts { status = model.JobStatusFailed finishedAt = &now } return s.db.Model(&model.AdminJob{}).Where("id = ?", id).Updates(map[string]interface{}{ "status": status, "error": message, "locked_by": "", "locked_until": nil, "finished_at": finishedAt, }).Error } func (s *Store) ReleaseJobLease(id uint) error { return s.db.Model(&model.AdminJob{}). Where("id = ? AND status = ?", id, model.JobStatusRunning). Updates(map[string]interface{}{ "status": model.JobStatusPending, "locked_by": "", "locked_until": nil, }).Error } func (s *Store) CancelJob(id uint) error { now := time.Now() result := s.db.Model(&model.AdminJob{}). Where("id = ? AND status IN ?", id, []string{model.JobStatusPending, model.JobStatusRunning}). Updates(map[string]interface{}{ "status": model.JobStatusCanceled, "locked_by": "", "locked_until": nil, "finished_at": &now, }) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return errors.New("job cannot be canceled") } return nil } func (s *Store) FindJobByID(id uint) (*model.AdminJob, error) { var job model.AdminJob if err := s.db.First(&job, id).Error; err != nil { return nil, err } return &job, nil } func firstTime(value *time.Time, fallback time.Time) *time.Time { if value != nil { return value } return &fallback }