package data import ( "context" "errors" "fmt" "math/rand" "strings" "time" "github.com/google/uuid" "github.com/meilisearch/meilisearch-go" "github.com/spf13/viper" "gorm.io/gorm" ) type Checkin struct { Id uint `json:"id" gorm:"primarykey;autoIncrement"` UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"` CheckinId uuid.UUID `json:"checkin_id" gorm:"type:uuid;uniqueIndex;not null"` EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"` UserId uuid.UUID `json:"user_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"` CheckinAt time.Time `json:"checkin_at"` } type CheckinSearchDoc struct { CheckinId string `json:"checkin_id"` EventId string `json:"event_id"` UserId string `json:"user_id"` CheckinAt time.Time `json:"checkin_at"` } func (self *Checkin) GetCheckin(userId, eventId uuid.UUID) (*Checkin, error) { var checkin Checkin err := Database. Where("user_id = ? AND event_id = ?", userId, eventId). First(&checkin).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil, nil } return nil, err } return &checkin, err } type CheckinUsers struct { UserId uuid.UUID `json:"user_id"` CheckinTime time.Time `json:"checkin_time"` } func (self *Checkin) GetUsersByEventID(eventID uuid.UUID) (*[]CheckinUsers, error) { var result []CheckinUsers err := Database. Model(&Checkin{}). Select("user_id, checkin_time"). Where("event_id = ?", eventID). Order("checkin_time ASC"). Scan(&result).Error return &result, err } type CheckinEvent struct { EventId uuid.UUID `json:"event_id"` CheckinTime time.Time `json:"checkin_time"` } func (self *Checkin) GetEventsByUserID(userID uuid.UUID) (*[]CheckinEvent, error) { var result []CheckinEvent err := Database. Model(&Checkin{}). Select("event_id, checkin_time"). Where("user_id = ?", userID). Order("checkin_time ASC"). Scan(&result).Error return &result, err } func (self *Checkin) CreateCheckin() error { self.UUID = uuid.New() self.CheckinId = uuid.New() // DB transaction for strong consistency err := Database.Transaction(func(tx *gorm.DB) error { if err := tx.Create(&self).Error; err != nil { return err } return nil }) if err != nil { return err } if err := self.UpdateSearchIndex(); err != nil { return err } return nil } type CheckinUpdateInput struct { CheckinTime *time.Time } func (self *Checkin) UpdateCheckin(checkinID uuid.UUID, input CheckinUpdateInput) (*Checkin, error) { var checkin Checkin err := Database.Transaction(func(tx *gorm.DB) error { // Lock the row for update if err := tx. Where("checkin_id = ?", checkinID). First(&checkin).Error; err != nil { return err } updates := map[string]any{} if input.CheckinTime != nil { updates["checkin_time"] = *input.CheckinTime } if len(updates) == 0 { return nil } if err := tx. Model(&checkin). Updates(updates).Error; err != nil { return err } // Reload to ensure struct is up to date return tx. Where("checkin_id = ?", checkinID). First(&checkin).Error }) if err != nil { return nil, err } // Sync to MeiliSearch (eventual consistency) if err := checkin.UpdateSearchIndex(); err != nil { return nil, err } return &checkin, nil } func (self *Checkin) SearchUsersByEvent(eventID string) (*meilisearch.SearchResponse, error) { index := MeiliSearch.Index("checkin") return index.Search("", &meilisearch.SearchRequest{ Filter: "event_id = \"" + eventID + "\"", Sort: []string{"checkin_time:asc"}, }) } func (self *Checkin) SearchEventsByUser(userID string) (*meilisearch.SearchResponse, error) { index := MeiliSearch.Index("checkin") return index.Search("", &meilisearch.SearchRequest{ Filter: "user_id = \"" + userID + "\"", Sort: []string{"checkin_time:asc"}, }) } func (self *Checkin) UpdateSearchIndex() error { doc := CheckinSearchDoc{ CheckinId: self.CheckinId.String(), EventId: self.EventId.String(), UserId: self.UserId.String(), CheckinAt: self.CheckinAt, } index := MeiliSearch.Index("checkin") primaryKey := "checkin_id" opts := &meilisearch.DocumentOptions{ PrimaryKey: &primaryKey, } if _, err := index.UpdateDocuments([]CheckinSearchDoc{doc}, opts); err != nil { return err } return nil } func (self *Checkin) DeleteSearchIndex() error { index := MeiliSearch.Index("checkin") _, err := index.DeleteDocument(self.CheckinId.String(), nil) return err } func (self *Checkin) GenCheckinCode(eventId uuid.UUID) (*string, error) { ctx := context.Background() ttl := viper.GetDuration("ttl.checkin_code_ttl") rng := rand.New(rand.NewSource(time.Now().UnixNano())) for { code := fmt.Sprintf("%06d", rng.Intn(900000)+100000) ok, err := Redis.SetNX( ctx, "checkin_code:"+code, "user_id:"+self.UserId.String()+":event_id:"+eventId.String(), ttl, ).Result() if err != nil { return nil, err } if ok { return &code, nil } } } func (self *Checkin) VerifyCheckinCode(checkinCode string) (*uuid.UUID, error) { ctx := context.Background() val, err := Redis.Get(ctx, "checkin_code:"+checkinCode).Result() if err != nil { return nil, errors.New("invalid or expired checkin code") } // Expected format: user_id::event_id: parts := strings.Split(val, ":") if len(parts) != 4 { return nil, errors.New("invalid checkin code format") } userIdStr := parts[1] eventIdStr := parts[3] userId, err := uuid.Parse(userIdStr) if err != nil { return nil, err } eventId, err := uuid.Parse(eventIdStr) if err != nil { return nil, err } // DB transaction: ensure checkin is created atomically err = Database.Transaction(func(tx *gorm.DB) error { checkin := &Checkin{ UUID: uuid.New(), CheckinId: uuid.New(), UserId: userId, EventId: eventId, CheckinAt: time.Now(), } if err := tx.Create(checkin).Error; err != nil { return err } // Sync search index after commit go func(c *Checkin) { _ = c.UpdateSearchIndex() }(checkin) // Consume the code (one-time use) Redis.Del(ctx, "checkin_code:"+checkinCode) return nil }) if err != nil { return nil, err } return &userId, nil }