Files
cms-server/data/attendance.go
Asai Neko e8406f731e
All checks were successful
Client CMS Check Build (NixCN CMS) TeamCity build finished
Backend Check Build (NixCN CMS) TeamCity build finished
Add checkin count in attendance and event api
Signed-off-by: Asai Neko <sugar@sne.moe>
2026-02-05 14:33:39 +08:00

271 lines
6.2 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) 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
}