296 lines
6.8 KiB
Go
296 lines
6.8 KiB
Go
package data
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"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"`
|
|
KycId uuid.UUID `json:"kyc_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"`
|
|
Role string `json:"role" gorm:"type:varchar(255);not null"`
|
|
State string `json:"state" gorm:"type:varchar(255);not null"` // suspended | out_of_limit | success
|
|
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) SetEventId(s uuid.UUID) *Attendance {
|
|
self.EventId = s
|
|
return self
|
|
}
|
|
|
|
func (self *Attendance) SetUserId(s uuid.UUID) *Attendance {
|
|
self.UserId = s
|
|
return self
|
|
}
|
|
|
|
func (self *Attendance) SetKycId(s uuid.UUID) *Attendance {
|
|
self.KycId = s
|
|
return self
|
|
}
|
|
|
|
func (self *Attendance) SetRole(s string) *Attendance {
|
|
self.Role = s
|
|
return self
|
|
}
|
|
|
|
func (self *Attendance) SetState(s string) *Attendance {
|
|
self.State = s
|
|
return self
|
|
}
|
|
|
|
func (self *Attendance) GetAttendance(ctx context.Context, userId, eventId uuid.UUID) (*Attendance, error) {
|
|
var checkin Attendance
|
|
|
|
err := Database.WithContext(ctx).
|
|
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(ctx context.Context, eventID uuid.UUID) (*[]AttendanceUsers, error) {
|
|
var result []AttendanceUsers
|
|
|
|
err := Database.WithContext(ctx).
|
|
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(ctx context.Context, userID uuid.UUID) (*[]AttendanceEvent, error) {
|
|
var result []AttendanceEvent
|
|
|
|
err := Database.WithContext(ctx).
|
|
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(ctx context.Context) error {
|
|
self.UUID = uuid.New()
|
|
self.AttendanceId = uuid.New()
|
|
|
|
// DB transaction for strong consistency
|
|
err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Create(&self).Error; err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *Attendance) GetAttendanceListByEventId(ctx context.Context, eventId uuid.UUID) (*[]Attendance, error) {
|
|
var result []Attendance
|
|
|
|
err := Database.WithContext(ctx).
|
|
Where("event_id = ?", eventId).
|
|
Order("checkin_at DESC").
|
|
Find(&result).Error
|
|
|
|
return &result, err
|
|
}
|
|
|
|
func (self *Attendance) GetJoinedEventIDs(ctx context.Context, userId uuid.UUID, eventIds []uuid.UUID) (map[uuid.UUID]bool, error) {
|
|
joinedMap := make(map[uuid.UUID]bool)
|
|
|
|
if len(eventIds) == 0 {
|
|
return joinedMap, nil
|
|
}
|
|
|
|
var foundEventIds []uuid.UUID
|
|
|
|
err := Database.WithContext(ctx).
|
|
Model(&Attendance{}).
|
|
Where("user_id = ? AND event_id IN ?", userId, eventIds).
|
|
Pluck("event_id", &foundEventIds).Error
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, id := range foundEventIds {
|
|
joinedMap[id] = true
|
|
}
|
|
|
|
return joinedMap, nil
|
|
}
|
|
|
|
func (self *Attendance) CountUsersByEventID(ctx context.Context, eventID uuid.UUID) (int64, error) {
|
|
var count int64
|
|
|
|
err := Database.WithContext(ctx).
|
|
Model(&Attendance{}).
|
|
Where("event_id = ?", eventID).
|
|
Count(&count).Error
|
|
|
|
if err == gorm.ErrRecordNotFound {
|
|
return 0, nil
|
|
}
|
|
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (self *Attendance) CountCheckedInUsersByEventID(ctx context.Context, eventID uuid.UUID) (int64, error) {
|
|
var count int64
|
|
|
|
err := Database.WithContext(ctx).
|
|
Model(&Attendance{}).
|
|
Where("event_id = ? AND checkin_at IS NOT NULL AND checkin_at > ?", eventID, time.Time{}). // 过滤未签到用户
|
|
Count(&count).Error
|
|
|
|
if err == gorm.ErrRecordNotFound {
|
|
return 0, nil
|
|
}
|
|
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (self *Attendance) Update(ctx context.Context, attendanceId uuid.UUID) (*Attendance, error) {
|
|
var attendance Attendance
|
|
|
|
err := Database.WithContext(ctx).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
|
|
}
|
|
|
|
if err := tx.
|
|
Model(&attendance).
|
|
Updates(self).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
|
|
}
|
|
|
|
return &attendance, nil
|
|
}
|
|
|
|
func (self *Attendance) GenCheckinCode(ctx context.Context, eventId uuid.UUID) (*string, error) {
|
|
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(ctx context.Context, checkinCode string) error {
|
|
val, err := Redis.Get(ctx, "checkin_code:"+checkinCode).Result()
|
|
if err != nil {
|
|
return errors.New("[Attendance Data] invalid or expired checkin code")
|
|
}
|
|
|
|
// Expected format: user_id:<uuid>:event_id:<uuid>
|
|
parts := strings.Split(val, ":")
|
|
if len(parts) != 4 {
|
|
return errors.New("[Attendance Data] 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(ctx, userId, eventId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
self.CheckinAt = time.Now()
|
|
_, err = self.Update(ctx, attendanceData.AttendanceId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|