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 Attendance struct { Id uint `json:"id" gorm:"primarykey;autoIncrement"` UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"` AttendanceId uuid.UUID `json:"attendance_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"` Role string `json:"role" gorm:"type:varchar(255);not null"` KycInfo string `json:"kyc_info" gorm:"type:text"` CheckinAt time.Time `json:"checkin_at"` } type AttendanceSearchDoc struct { AttendanceId string `json:"attendance_id"` EventId string `json:"event_id"` UserId string `json:"user_id"` Role string `json:"role"` CheckinAt time.Time `json:"checkin_at"` } func (self *Attendance) GetAttendance(userId, eventId uuid.UUID) (*Attendance, error) { var checkin Attendance 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 AttendanceUsers struct { UserId uuid.UUID `json:"user_id"` Role string `json:"role"` CheckinAt time.Time `json:"checkin_at"` } func (self *Attendance) GetUsersByEventID(eventID uuid.UUID) (*[]AttendanceUsers, error) { var result []AttendanceUsers err := Database. Model(&Attendance{}). Select("user_id, checkin_at"). Where("event_id = ?", eventID). Order("checkin_at ASC"). Scan(&result).Error return &result, err } type AttendanceEvent struct { EventId uuid.UUID `json:"event_id"` CheckinAt time.Time `json:"checkin_at"` } func (self *Attendance) GetEventsByUserID(userID uuid.UUID) (*[]AttendanceEvent, error) { var result []AttendanceEvent err := Database. Model(&Attendance{}). Select("event_id, checkin_at"). Where("user_id = ?", userID). Order("checkin_at ASC"). Scan(&result).Error return &result, err } func (self *Attendance) Create() error { self.UUID = uuid.New() self.AttendanceId = 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 } func (self *Attendance) Update(attendanceId uuid.UUID, checkinTime *time.Time) (*Attendance, error) { var attendance Attendance err := Database.Transaction(func(tx *gorm.DB) error { // Lock the row for update if err := tx. Where("attendance_id = ?", attendanceId). First(&attendance).Error; err != nil { return err } updates := map[string]any{} if checkinTime != nil { updates["checkin_at"] = *checkinTime } if len(updates) == 0 { return nil } if err := tx. Model(&attendance). Updates(updates).Error; err != nil { return err } // Reload to ensure struct is up to date return tx. Where("attendance_id = ?", attendanceId). First(&attendance).Error }) if err != nil { return nil, err } // Sync to MeiliSearch (eventual consistency) if err := attendance.UpdateSearchIndex(); err != nil { return nil, err } return &attendance, nil } func (self *Attendance) SearchUsersByEvent(eventID string) (*meilisearch.SearchResponse, error) { index := MeiliSearch.Index("attendance") return index.Search("", &meilisearch.SearchRequest{ Filter: "event_id = \"" + eventID + "\"", Sort: []string{"checkin_at:asc"}, }) } func (self *Attendance) SearchEventsByUser(userID string) (*meilisearch.SearchResponse, error) { index := MeiliSearch.Index("attendance") return index.Search("", &meilisearch.SearchRequest{ Filter: "user_id = \"" + userID + "\"", Sort: []string{"checkin_at:asc"}, }) } func (self *Attendance) UpdateSearchIndex() error { doc := AttendanceSearchDoc{ AttendanceId: self.AttendanceId.String(), EventId: self.EventId.String(), UserId: self.UserId.String(), CheckinAt: self.CheckinAt, } index := MeiliSearch.Index("attendance") primaryKey := "attendance_id" opts := &meilisearch.DocumentOptions{ PrimaryKey: &primaryKey, } if _, err := index.UpdateDocuments([]AttendanceSearchDoc{doc}, opts); err != nil { return err } return nil } func (self *Attendance) DeleteSearchIndex() error { index := MeiliSearch.Index("attendance") _, err := index.DeleteDocument(self.AttendanceId.String(), nil) return err } func (self *Attendance) 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 *Attendance) VerifyCheckinCode(checkinCode string) error { ctx := context.Background() val, err := Redis.Get(ctx, "checkin_code:"+checkinCode).Result() if err != nil { return errors.New("invalid or expired checkin code") } // Expected format: user_id::event_id: parts := strings.Split(val, ":") if len(parts) != 4 { return errors.New("invalid checkin code format") } userIdStr := parts[1] eventIdStr := parts[3] userId, err := uuid.Parse(userIdStr) if err != nil { return err } eventId, err := uuid.Parse(eventIdStr) if err != nil { return err } attendanceData, err := self.GetAttendance(userId, eventId) if err != nil { return err } time := time.Now() _, err = self.Update(attendanceData.AttendanceId, &time) if err != nil { return err } return nil }