2026-05-12 09:53:20 +08:00

262 lines
7.2 KiB
Go

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_ms 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
}
nowMS := time.Now().UTC().UnixMilli()
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_ms IS NOT NULL AND locked_until_ms < ?)) AND attempt < max_attempts", model.JobStatusPending, model.JobStatusRunning, nowMS).
Order("created_at_ms ASC, id ASC").
First(&job).Error
if err != nil {
return err
}
if job.MaxAttempts <= 0 {
job.MaxAttempts = maxAttempts
}
updates := map[string]any{
"status": model.JobStatusRunning,
"attempt": job.Attempt + 1,
"max_attempts": job.MaxAttempts,
"locked_by": workerID,
"locked_until_ms": nowMS + lease.Milliseconds(),
"started_at_ms": firstMS(job.StartedAtMS, nowMS),
"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 {
nowMS := time.Now().UTC().UnixMilli()
return s.db.Model(&model.AdminJob{}).Where("id = ?", id).Updates(map[string]any{
"status": model.JobStatusSucceeded,
"result_json": resultJSON,
"artifact_path": artifactPath,
"locked_by": "",
"locked_until_ms": nil,
"finished_at_ms": &nowMS,
}).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
}
nowMS := time.Now().UTC().UnixMilli()
status := model.JobStatusPending
finishedAt := any(nil)
if job.Attempt >= job.MaxAttempts {
status = model.JobStatusFailed
finishedAt = &nowMS
}
return s.db.Model(&model.AdminJob{}).Where("id = ?", id).Updates(map[string]any{
"status": status,
"error": message,
"locked_by": "",
"locked_until_ms": nil,
"finished_at_ms": 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]any{
"status": model.JobStatusPending,
"locked_by": "",
"locked_until_ms": nil,
}).Error
}
func (s *Store) CancelJob(id uint) error {
nowMS := time.Now().UTC().UnixMilli()
result := s.db.Model(&model.AdminJob{}).
Where("id = ? AND status IN ?", id, []string{model.JobStatusPending, model.JobStatusRunning}).
Updates(map[string]any{
"status": model.JobStatusCanceled,
"locked_by": "",
"locked_until_ms": nil,
"finished_at_ms": &nowMS,
})
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 firstMS(value *int64, fallback int64) *int64 {
if value != nil {
return value
}
return &fallback
}