package subject import ( "context" "errors" "fmt" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgconn" "gorm.io/gorm" "gorm.io/gorm/clause" ) type Repository struct { db *gorm.DB } type subjectRow struct { ID uuid.UUID `gorm:"column:id;type:uuid;primaryKey"` PlatformName string `gorm:"column:platform_name"` Workspace string `gorm:"column:workspace"` CreatedAt time.Time `gorm:"column:created_at"` UpdatedAt time.Time `gorm:"column:updated_at"` LicenceCount int64 `gorm:"column:licence_count;->"` } func (subjectRow) TableName() string { return "authorization_subjects" } func NewRepository(db *gorm.DB) (*Repository, error) { if db == nil { return nil, ErrInvalidInput } return &Repository{db: db}, nil } func (r *Repository) Create(ctx context.Context, item Subject) (Subject, error) { row := rowFromSubject(item) if err := r.db.WithContext(ctx).Create(&row).Error; err != nil { return Subject{}, mapDatabaseError(err) } return row.subject(), nil } func (r *Repository) Get(ctx context.Context, id uuid.UUID) (Subject, error) { var row subjectRow err := r.db.WithContext(ctx). Table("authorization_subjects AS subjects"). Select(`subjects.id, subjects.platform_name, subjects.workspace, subjects.created_at, subjects.updated_at, (SELECT COUNT(*) FROM licence_issuances WHERE subject_id = subjects.id) AS licence_count`). Where("subjects.id = ?", id). Take(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return Subject{}, ErrNotFound } if err != nil { return Subject{}, err } return row.subject(), nil } func (r *Repository) GetForUpdate(ctx context.Context, tx *gorm.DB, id uuid.UUID) (Subject, error) { if r == nil || r.db == nil || ctx == nil || tx == nil || id == uuid.Nil { return Subject{}, ErrInvalidInput } row, err := lockSubject(tx.WithContext(ctx), id) if err != nil { return Subject{}, err } return row.subject(), nil } func (r *Repository) List(ctx context.Context, query string, page, pageSize int) ([]Subject, int64, error) { base := r.db.WithContext(ctx).Table("authorization_subjects AS subjects") if query != "" { base = base.Where( "STRPOS(LOWER(subjects.platform_name), LOWER(?)) > 0 OR STRPOS(LOWER(subjects.workspace), LOWER(?)) > 0", query, query, ) } var total int64 if err := base.Count(&total).Error; err != nil { return nil, 0, err } var rows []subjectRow err := base. Select(`subjects.id, subjects.platform_name, subjects.workspace, subjects.created_at, subjects.updated_at, (SELECT COUNT(*) FROM licence_issuances WHERE subject_id = subjects.id) AS licence_count`). Order("subjects.created_at DESC, subjects.id DESC"). Offset((page - 1) * pageSize). Limit(pageSize). Find(&rows).Error if err != nil { return nil, 0, err } items := make([]Subject, len(rows)) for index := range rows { items[index] = rows[index].subject() } return items, total, nil } func (r *Repository) UpdateIfUnsigned(ctx context.Context, id uuid.UUID, input UpsertInput) (Subject, error) { var updated Subject err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { row, err := lockSubject(tx, id) if err != nil { return err } licenceCount, err := countLicences(tx, id) if err != nil { return err } if licenceCount != 0 { return ErrHasLicences } row.PlatformName = input.PlatformName row.Workspace = input.Workspace row.UpdatedAt = time.Now().UTC() result := tx.Model(&subjectRow{}). Where("id = ?", id). Updates(map[string]any{ "platform_name": row.PlatformName, "workspace": row.Workspace, "updated_at": row.UpdatedAt, }) if result.Error != nil { return mapDatabaseError(result.Error) } if result.RowsAffected != 1 { return ErrNotFound } updated = row.subject() return nil }) if err != nil { return Subject{}, err } return updated, nil } func (r *Repository) DeleteIfUnsigned(ctx context.Context, id uuid.UUID) error { return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { row, err := lockSubject(tx, id) if err != nil { return err } licenceCount, err := countLicences(tx, id) if err != nil { return err } if licenceCount != 0 { return ErrHasLicences } result := tx.Delete(&row) if result.Error != nil { return mapDatabaseError(result.Error) } if result.RowsAffected != 1 { return ErrNotFound } return nil }) } func lockSubject(tx *gorm.DB, id uuid.UUID) (subjectRow, error) { var row subjectRow err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", id).Take(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return subjectRow{}, ErrNotFound } if err != nil { return subjectRow{}, err } return row, nil } func countLicences(tx *gorm.DB, id uuid.UUID) (int64, error) { var count int64 if err := tx.Table("licence_issuances").Where("subject_id = ?", id).Count(&count).Error; err != nil { return 0, fmt.Errorf("统计授权对象的许可证记录: %w", err) } return count, nil } func mapDatabaseError(err error) error { var postgresError *pgconn.PgError if errors.As(err, &postgresError) { switch postgresError.Code { case "23505": return ErrDuplicate case "23503": return ErrHasLicences } } return err } func rowFromSubject(item Subject) subjectRow { return subjectRow{ ID: item.ID, PlatformName: item.PlatformName, Workspace: item.Workspace, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, LicenceCount: item.LicenceCount, } } func (r subjectRow) subject() Subject { return Subject{ ID: r.ID, PlatformName: r.PlatformName, Workspace: r.Workspace, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt, LicenceCount: r.LicenceCount, } }