From acd3c95c807be99faf616bd98e1e552e23651af4 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Thu, 1 Jan 2026 13:29:59 +0800 Subject: [PATCH] Refactor mass data structure Signed-off-by: Asai Neko --- data/checkin.go | 281 +++++++++++++++++++++++++++++++++++++++ data/client.go | 1 + data/event.go | 172 ++++++++++++------------ data/user.go | 233 ++++++++++++-------------------- service/auth/handler.go | 1 - service/auth/magic.go | 4 +- service/auth/redirect.go | 1 + service/auth/token.go | 1 + service/user/checkin.go | 29 ++-- service/user/info.go | 27 ++-- service/user/query.go | 47 +++++-- 11 files changed, 518 insertions(+), 279 deletions(-) create mode 100644 data/checkin.go create mode 100644 data/client.go create mode 100644 service/auth/redirect.go create mode 100644 service/auth/token.go diff --git a/data/checkin.go b/data/checkin.go new file mode 100644 index 0000000..0f9e3a7 --- /dev/null +++ b/data/checkin.go @@ -0,0 +1,281 @@ +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 +} diff --git a/data/client.go b/data/client.go new file mode 100644 index 0000000..0ad59c2 --- /dev/null +++ b/data/client.go @@ -0,0 +1 @@ +package data diff --git a/data/event.go b/data/event.go index e33450c..74a0126 100644 --- a/data/event.go +++ b/data/event.go @@ -1,26 +1,21 @@ package data import ( - "errors" - "slices" "time" "github.com/go-viper/mapstructure/v2" "github.com/google/uuid" "github.com/meilisearch/meilisearch-go" - "gorm.io/datatypes" "gorm.io/gorm" - "gorm.io/gorm/clause" ) type Event struct { - Id uint `json:"id" gorm:"primarykey;autoincrement"` - UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"` - EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex;not null"` - Name string `json:"name" gorm:"type:varchar(255);index;not null"` - StartTime time.Time `json:"start_time" gorm:"index"` - EndTime time.Time `json:"end_time" gorm:"index"` - JoinedUsers datatypes.JSONSlice[uuid.UUID] `json:"joined_users"` + Id uint `json:"id" gorm:"primarykey;autoincrement"` + UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"` + EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex;not null"` + Name string `json:"name" gorm:"type:varchar(255);index;not null"` + StartTime time.Time `json:"start_time" gorm:"index"` + EndTime time.Time `json:"end_time" gorm:"index"` } type EventSearchDoc struct { @@ -30,93 +25,71 @@ type EventSearchDoc struct { EndTime time.Time `json:"end_time"` } -func (self *Event) GetEventById(eventId uuid.UUID) error { - return Database.Transaction(func(tx *gorm.DB) error { - if err := tx.Where("event_id = ?", eventId).First(&self).Error; err != nil { - return err +func (self *Event) GetEventById(eventId uuid.UUID) (*Event, error) { + var event Event + + err := Database. + Where("event_id = ?", eventId). + First(&event).Error + + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil } - return nil - }) + return nil, err + } + + return &event, nil } func (self *Event) UpdateEventById(eventId uuid.UUID) error { - return Database.Transaction(func(tx *gorm.DB) error { - if err := tx.Model(&Event{}).Where("event_id = ?", eventId).Updates(&self).Error; err != nil { + // DB transaction + if err := Database.Transaction(func(tx *gorm.DB) error { + // Update by business key + if err := tx. + Model(&Event{}). + Where("event_id = ?", eventId). + Updates(self).Error; err != nil { return err } - // Update event to document index - doc := EventSearchDoc{ - EventId: self.EventId.String(), - Name: self.Name, - StartTime: self.StartTime, - EndTime: self.EndTime, - } - index := MeiliSearch.Index("event") - docPrimaryKey := "event_id" - meiliOptions := &meilisearch.DocumentOptions{PrimaryKey: &docPrimaryKey} - if _, err := index.UpdateDocuments([]EventSearchDoc{doc}, meiliOptions); err != nil { - return err - } + // Reload to ensure struct is fresh + return tx. + Where("event_id = ?", eventId). + First(self).Error + }); err != nil { + return err + } - return nil - }) + // Sync search index + if err := self.UpdateSearchIndex(); err != nil { + return err + } + + return nil } func (self *Event) CreateEvent() error { - if self.UUID == uuid.Nil { - self.UUID = uuid.New() - } - if self.EventId == uuid.Nil { - self.EventId = uuid.New() + self.UUID = uuid.New() + self.EventId = uuid.New() + + // DB transaction only + if err := Database.Transaction(func(tx *gorm.DB) error { + if err := tx.Create(self).Error; err != nil { + return err + } + return nil + }); err != nil { + return err } - return Database.Transaction(func(tx *gorm.DB) error { - if err := tx.Create(&self).Error; err != nil { - return err - } + // Search index (eventual consistency) + if err := self.UpdateSearchIndex(); err != nil { + // TODO: async retry / log + return err + } - // Add event to document index - doc := EventSearchDoc{ - EventId: self.EventId.String(), - Name: self.Name, - StartTime: self.StartTime, - EndTime: self.EndTime, - } - index := MeiliSearch.Index("event") - docPrimaryKey := "event_id" - meiliOptions := &meilisearch.DocumentOptions{PrimaryKey: &docPrimaryKey} - if _, err := index.AddDocuments([]EventSearchDoc{doc}, meiliOptions); err != nil { - return err - } - - return nil - }) -} - -func (self *Event) UserJoinEvent(userId, eventId uuid.UUID) error { - return Database.Transaction(func(tx *gorm.DB) error { - var event Event - if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). - Where("event_id = ?", eventId). - First(&event).Error; err != nil { - return err - } - - // Check if user already joined - if slices.Contains(event.JoinedUsers, userId) { - return errors.New("user already joined") - } - - // Add user to list - event.JoinedUsers = append(event.JoinedUsers, userId) - if err := tx.Model(&Event{}).Where("event_id = ?", eventId).Update("joined_users", event.JoinedUsers).Error; err != nil { - return err - } - - *self = event - return nil - }) + return nil } func (self *Event) GetFullTable() (*[]Event, error) { @@ -130,6 +103,8 @@ func (self *Event) GetFullTable() (*[]Event, error) { func (self *Event) FastListEvents(limit, offset int64) (*[]EventSearchDoc, error) { index := MeiliSearch.Index("event") + + // Fast read from MeiliSearch (no DB involved) result, err := index.Search("", &meilisearch.SearchRequest{ Limit: limit, Offset: offset, @@ -137,9 +112,38 @@ func (self *Event) FastListEvents(limit, offset int64) (*[]EventSearchDoc, error if err != nil { return nil, err } + var list []EventSearchDoc if err := mapstructure.Decode(result.Hits, &list); err != nil { return nil, err } + return &list, nil } + +func (self *Event) UpdateSearchIndex() error { + doc := EventSearchDoc{ + EventId: self.EventId.String(), + Name: self.Name, + StartTime: self.StartTime, + EndTime: self.EndTime, + } + index := MeiliSearch.Index("event") + + primaryKey := "event_id" + opts := &meilisearch.DocumentOptions{ + PrimaryKey: &primaryKey, + } + + if _, err := index.UpdateDocuments([]EventSearchDoc{doc}, opts); err != nil { + return err + } + + return nil +} + +func (self *Event) DeleteSearchIndex() error { + index := MeiliSearch.Index("event") + _, err := index.DeleteDocument(self.EventId.String(), nil) + return err +} diff --git a/data/user.go b/data/user.go index a63737f..02a0ee3 100644 --- a/data/user.go +++ b/data/user.go @@ -1,20 +1,10 @@ package data import ( - "context" - "errors" - "fmt" - "math/rand" - "strings" - "time" - "github.com/go-viper/mapstructure/v2" "github.com/google/uuid" "github.com/meilisearch/meilisearch-go" - "github.com/spf13/viper" - "gorm.io/datatypes" "gorm.io/gorm" - "gorm.io/gorm/clause" ) // Permission Level @@ -24,16 +14,15 @@ import ( // Super User: 30 type User struct { - Id uint `json:"id" gorm:"primarykey;autoincrement"` - UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueindex;not null"` - UserId uuid.UUID `json:"user_id" gorm:"type:uuid;uniqueindex;not null"` - Email string `json:"email" gorm:"type:varchar(255);uniqueindex;not null"` - Type string `json:"type" gorm:"type:varchar(32);index;not null"` - Nickname string `json:"nickname"` - Subtitle string `json:"subtitle"` - Avatar string `json:"avatar"` - Checkin datatypes.JSONMap `json:"checkin"` - PermissionLevel uint `json:"permission_level" gorm:"default:10;not null"` + Id uint `json:"id" gorm:"primarykey;autoincrement"` + UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueindex;not null"` + UserId uuid.UUID `json:"user_id" gorm:"type:uuid;uniqueindex;not null"` + Email string `json:"email" gorm:"type:varchar(255);uniqueindex;not null"` + Type string `json:"type" gorm:"type:varchar(32);index;not null"` + Nickname string `json:"nickname"` + Subtitle string `json:"subtitle"` + Avatar string `json:"avatar"` + PermissionLevel uint `json:"permission_level" gorm:"default:10;not null"` } type UserSearchDoc struct { @@ -46,88 +35,61 @@ type UserSearchDoc struct { PermissionLevel uint `json:"permission_level"` } -func (self *User) GetByEmail(email string) error { - if err := Database.Where("email = ?", email).First(&self).Error; err != nil { - return err +func (self *User) GetByEmail(email string) (*User, error) { + var user User + + err := Database. + Where("email = ?", email). + First(&user).Error + + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err } - return nil + + return &user, nil } -func (self *User) GetByUserId(userId uuid.UUID) error { - if err := Database.Where("user_id = ?", userId).First(&self).Error; err != nil { - return err +func (self *User) GetByUserId(userId uuid.UUID) (*User, error) { + var user User + + err := Database. + Where("user_id = ?", userId). + First(&user).Error + + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err } - return nil -} -func (self *User) UpdateCheckin(userId, eventId uuid.UUID, time time.Time) error { - return Database.Transaction(func(tx *gorm.DB) error { - if err := tx. - Clauses(clause.Locking{Strength: "UPDATE"}). - Where("user_id = ?", userId). - First(self).Error; err != nil { - return err // if error then rollback - } - - self.Checkin = datatypes.JSONMap{eventId.String(): time} - - if err := tx.Save(self).Error; err != nil { - return err // rollback - } - - // Update user to document index - doc := UserSearchDoc{ - UserId: self.UserId.String(), - Email: self.Email, - Type: self.Type, - Nickname: self.Nickname, - Subtitle: self.Subtitle, - Avatar: self.Avatar, - PermissionLevel: self.PermissionLevel, - } - index := MeiliSearch.Index("user") - docPrimaryKey := "user_id" - meiliOptions := &meilisearch.DocumentOptions{PrimaryKey: &docPrimaryKey} - if _, err := index.UpdateDocuments([]UserSearchDoc{doc}, meiliOptions); err != nil { - return err - } - - return nil // commit - }) + return &user, err } func (self *User) Create() error { - return Database.Transaction(func(tx *gorm.DB) error { - if self.UUID == uuid.Nil { - self.UUID = uuid.New() - } - if self.UserId == uuid.Nil { - self.UserId = uuid.New() - } + self.UUID = uuid.New() + self.UserId = uuid.New() + // DB transaction only + if err := Database.Transaction(func(tx *gorm.DB) error { if err := tx.Create(self).Error; err != nil { return err } - - // Create user to document index - doc := UserSearchDoc{ - UserId: self.UserId.String(), - Email: self.Email, - Type: self.Type, - Nickname: self.Nickname, - Subtitle: self.Subtitle, - Avatar: self.Avatar, - PermissionLevel: self.PermissionLevel, - } - index := MeiliSearch.Index("user") - docPrimaryKey := "user_id" - meiliOptions := &meilisearch.DocumentOptions{PrimaryKey: &docPrimaryKey} - if _, err := index.AddDocuments([]UserSearchDoc{doc}, meiliOptions); err != nil { - return err - } - return nil - }) + }); err != nil { + return err + } + + // Search index (eventual consistency) + if err := self.UpdateSearchIndex(); err != nil { + // TODO: async retry / log + return err + } + + return nil } func (self *User) UpdateByUserID(userId uuid.UUID) error { @@ -150,6 +112,8 @@ func (self *User) GetFullTable() (*[]User, error) { func (self *User) FastListUsers(limit, offset int64) (*[]UserSearchDoc, error) { index := MeiliSearch.Index("user") + + // Fast read from MeiliSearch, no DB involved result, err := index.Search("", &meilisearch.SearchRequest{ Limit: limit, Offset: offset, @@ -157,75 +121,44 @@ func (self *User) FastListUsers(limit, offset int64) (*[]UserSearchDoc, error) { if err != nil { return nil, err } + var list []UserSearchDoc if err := mapstructure.Decode(result.Hits, &list); err != nil { return nil, err } + return &list, nil } -func (self *User) 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 *User) UpdateSearchIndex() error { + doc := UserSearchDoc{ + UserId: self.UserId.String(), + Email: self.Email, + Type: self.Type, + Nickname: self.Nickname, + Subtitle: self.Subtitle, + Avatar: self.Avatar, + PermissionLevel: self.PermissionLevel, } + index := MeiliSearch.Index("user") + + primaryKey := "user_id" + opts := &meilisearch.DocumentOptions{ + PrimaryKey: &primaryKey, + } + + if _, err := index.UpdateDocuments( + []UserSearchDoc{doc}, + opts, + ); err != nil { + return err + } + + return nil } -func (self *User) VerifyCheckinCode(checkinCode string) (*uuid.UUID, error) { - ctx := context.Background() - - result := Redis.Get(ctx, "checkin_code:"+checkinCode).String() - - if result == "" { - return nil, errors.New("invalid or expired checkin code") - } - - split := strings.Split(result, ":") - if len(split) < 2 { - return nil, errors.New("invalid checkin code format") - } - - userId := split[0] - eventId := split[1] - - var returnedUserId uuid.UUID - err := Database.Transaction(func(tx *gorm.DB) error { - checkinData := map[string]interface{}{ - eventId: time.Now(), - } - - if err := tx.Model(&User{}).Where("user_id = ?", userId).Updates(map[string]interface{}{ - "checkin": checkinData, - }).Error; err != nil { - return err - } - - parsedUserId, err := uuid.Parse(userId) - if err != nil { - return err - } - returnedUserId = parsedUserId - return nil - }) - - if err != nil { - return nil, err - } - - return &returnedUserId, nil +func (self *User) DeleteSearchIndex() error { + index := MeiliSearch.Index("user") + _, err := index.DeleteDocument(self.UserId.String(), nil) + return err } diff --git a/service/auth/handler.go b/service/auth/handler.go index 0dff602..1d875be 100644 --- a/service/auth/handler.go +++ b/service/auth/handler.go @@ -4,6 +4,5 @@ import "github.com/gin-gonic/gin" func Handler(r *gin.RouterGroup) { r.POST("/magic", RequestMagicLink) - r.GET("/magic/verify", VerifyMagicLink) r.POST("/refresh", Refresh) } diff --git a/service/auth/magic.go b/service/auth/magic.go index 8ba2271..2f8f67e 100644 --- a/service/auth/magic.go +++ b/service/auth/magic.go @@ -81,8 +81,8 @@ func VerifyMagicLink(c *gin.Context) { } // Verify if user exists - user := new(data.User) - err := user.GetByEmail(email) + userData := new(data.User) + user, err := userData.GetByEmail(email) if err != nil { if err == gorm.ErrRecordNotFound { diff --git a/service/auth/redirect.go b/service/auth/redirect.go new file mode 100644 index 0000000..8832b06 --- /dev/null +++ b/service/auth/redirect.go @@ -0,0 +1 @@ +package auth diff --git a/service/auth/token.go b/service/auth/token.go new file mode 100644 index 0000000..8832b06 --- /dev/null +++ b/service/auth/token.go @@ -0,0 +1 @@ +package auth diff --git a/service/user/checkin.go b/service/user/checkin.go index 07f50fa..23910e7 100644 --- a/service/user/checkin.go +++ b/service/user/checkin.go @@ -8,7 +8,7 @@ import ( ) func Checkin(c *gin.Context) { - data := new(data.User) + data := new(data.Checkin) userId, ok := c.Get("user_id") if !ok { c.JSON(401, gin.H{ @@ -50,13 +50,29 @@ func Checkin(c *gin.Context) { } func CheckinSubmit(c *gin.Context) { + userId, ok := c.Get("user_id") + if !ok { + c.JSON(403, gin.H{ + "status": "unauthorized", + }) + } + + userData := new(data.User) + userData.GetByUserId(userId.(uuid.UUID)) + if userData.PermissionLevel <= 20 { + c.JSON(403, gin.H{ + "status": "access denied", + }) + return + } + var req struct { ChekinCode string `json:"checkin_code"` } c.ShouldBindJSON(&req) - data := new(data.User) - userId, err := data.VerifyCheckinCode(req.ChekinCode) + checkinData := new(data.Checkin) + userId, err := checkinData.VerifyCheckinCode(req.ChekinCode) if err != nil { c.JSON(400, gin.H{ "status": "error verify checkin code", @@ -64,13 +80,6 @@ func CheckinSubmit(c *gin.Context) { return } - data.GetByUserId(*userId) - if data.PermissionLevel <= 20 { - c.JSON(403, gin.H{ - "status": "access denied", - }) - } - c.JSON(200, gin.H{ "status": "success", }) diff --git a/service/user/info.go b/service/user/info.go index 57a607d..c6fca78 100644 --- a/service/user/info.go +++ b/service/user/info.go @@ -2,14 +2,13 @@ package user import ( "nixcn-cms/data" - "time" "github.com/gin-gonic/gin" "github.com/google/uuid" ) func Info(c *gin.Context) { - data := new(data.User) + userData := new(data.User) userId, ok := c.Get("user_id") if !ok { c.JSON(404, gin.H{ @@ -19,7 +18,7 @@ func Info(c *gin.Context) { } // Get user from database - err := data.GetByUserId(userId.(uuid.UUID)) + user, err := userData.GetByUserId(userId.(uuid.UUID)) if err != nil { c.JSON(404, gin.H{ "status": "user not found", @@ -27,21 +26,13 @@ func Info(c *gin.Context) { return } - // Set time nil if time is zero - for k, v := range data.Checkin { - if t, ok := v.(time.Time); ok && t.IsZero() { - data.Checkin[k] = nil - } - } - c.JSON(200, gin.H{ - "user_id": data.UserId, - "email": data.Email, - "type": data.Type, - "nickname": data.Nickname, - "subtitle": data.Subtitle, - "avatar": data.Avatar, - "checkin": data.Checkin, - "permission_level": data.PermissionLevel, + "user_id": user.UserId, + "email": user.Email, + "type": user.Type, + "nickname": user.Nickname, + "subtitle": user.Subtitle, + "avatar": user.Avatar, + "permission_level": user.PermissionLevel, }) } diff --git a/service/user/query.go b/service/user/query.go index 9dd9117..ed2224f 100644 --- a/service/user/query.go +++ b/service/user/query.go @@ -1,11 +1,13 @@ package user import ( + "errors" "nixcn-cms/data" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" + "gorm.io/gorm" ) func Query(c *gin.Context) { @@ -14,29 +16,46 @@ func Query(c *gin.Context) { c.JSON(400, gin.H{"status": "could not found user_id"}) return } - eventId, ok := c.GetQuery("event_id") + + eventIdOrig, ok := c.GetQuery("event_id") if !ok { c.JSON(400, gin.H{"status": "could not found event_id"}) return } - - data := new(data.User) - err := data.GetByUserId(userId.(uuid.UUID)) + eventId, err := uuid.Parse(eventIdOrig) if err != nil { - c.JSON(404, gin.H{"status": "cannot found user"}) - return - } - if data.Checkin[eventId] == nil { - c.JSON(404, gin.H{"status": "cannot found user checked in"}) + c.JSON(400, gin.H{"status": "event_id is not valid"}) return } - var checkinTime *time.Time - if data.Checkin[eventId].(*time.Time).IsZero() { - checkinTime = nil - } else { - checkinTime = data.Checkin[eventId].(*time.Time) + checkinData := new(data.Checkin) + checkin, err := checkinData.GetCheckin(userId.(uuid.UUID), eventId) + if err != nil { + c.JSON(500, gin.H{"status": "database error"}) + return + } else if checkin == nil { + c.JSON(404, gin.H{"status": "event checkin record not found"}) + return } + + checkinTime := time.Now() + checkinData.EventId = eventId + checkinData.UserId = userId.(uuid.UUID) + checkinData.CheckinAt = checkinTime + err = checkinData.CreateCheckin() + if err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + c.JSON(409, gin.H{ + "status": "already checked in", + }) + return + } + c.JSON(500, gin.H{ + "status": "database error", + }) + return + } + c.JSON(200, gin.H{ "checkin_time": checkinTime, })