feat: add party-size queueing and call modes
This commit is contained in:
@@ -20,11 +20,12 @@ import (
|
||||
)
|
||||
|
||||
type projectSeed struct {
|
||||
Code, Name, Status, Prefix, ETAMode, DeviceMode, DisplayToken string
|
||||
BatchSize, Grace, BatchInterval, Buffer int
|
||||
Rate float64
|
||||
Tickets []ticketSeed
|
||||
SessionStatus string
|
||||
Code, Name, Status, Prefix, ETAMode, DeviceMode, DisplayToken, CallMode string
|
||||
BatchSize, MaxTicketCall, DefaultPeopleCall, MaxPeopleCall int
|
||||
MinPartySize, MaxPartySize, Grace, BatchInterval, Buffer int
|
||||
Rate float64
|
||||
Tickets []ticketSeed
|
||||
SessionStatus string
|
||||
}
|
||||
|
||||
type ticketSeed struct {
|
||||
@@ -86,7 +87,8 @@ func main() {
|
||||
if index == 2 && user.Role == model.RoleStaff {
|
||||
continue
|
||||
}
|
||||
if err := tx.Create(&model.UserProject{UserID: user.ID, ProjectID: project.ID, CreatedAt: now}).Error; err != nil {
|
||||
assignment := model.UserProject{UserID: user.ID, ProjectID: project.ID, CreatedAt: now}
|
||||
if err := tx.Where("user_id = ? AND project_id = ?", user.ID, project.ID).FirstOrCreate(&assignment).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -112,15 +114,15 @@ func main() {
|
||||
|
||||
func demoProjects() []projectSeed {
|
||||
projects := []projectSeed{
|
||||
{Code: envOr("PROJECT_CODE", "DEMO"), Name: envOr("PROJECT_NAME", "云栖观光车"), Status: model.ProjectRunning, Prefix: "A", ETAMode: model.ETAFixedBatch, DeviceMode: "SUCCESS", DisplayToken: envOr("DEMO_DISPLAY_TOKEN", "demo-display-yunqi"), BatchSize: 5, Grace: 5, BatchInterval: 480, Buffer: 3, Rate: 2, SessionStatus: "RUNNING", Tickets: []ticketSeed{
|
||||
{Code: envOr("PROJECT_CODE", "DEMO"), Name: envOr("PROJECT_NAME", "云栖观光车"), Status: model.ProjectRunning, Prefix: "A", ETAMode: model.ETAFixedBatch, DeviceMode: "SUCCESS", DisplayToken: envOr("DEMO_DISPLAY_TOKEN", "demo-display-yunqi"), CallMode: model.CallModeBoth, BatchSize: 5, MaxTicketCall: 20, DefaultPeopleCall: 10, MaxPeopleCall: 50, MinPartySize: 1, MaxPartySize: 10, Grace: 5, BatchInterval: 480, Buffer: 3, Rate: 2, SessionStatus: "RUNNING", Tickets: []ticketSeed{
|
||||
{1, "13800001001", "张", "先生", model.TicketCompleted, 95, 1}, {2, "13800001002", "李", "女士", model.TicketCompleted, 93, 1}, {3, "13800001003", "王", "先生", model.TicketCompleted, 91, 1}, {4, "13800001004", "赵", "女士", model.TicketCompleted, 89, 1}, {5, "13800001005", "陈", "先生", model.TicketCompleted, 87, 1},
|
||||
{6, "13800001006", "刘", "女士", model.TicketMissed, 72, 2}, {7, "13800001007", "杨", "先生", model.TicketCalled, 18, 2}, {8, "13800001008", "黄", "女士", model.TicketCalled, 17, 2}, {9, "13800001009", "周", "先生", model.TicketCalled, 16, 2}, {10, "13800001010", "吴", "女士", model.TicketArrived, 15, 2},
|
||||
{11, "13800001011", "徐", "先生", model.TicketWaiting, 14, 0}, {12, "13800001012", "孙", "女士", model.TicketWaiting, 13, 0}, {13, "13800001013", "胡", "先生", model.TicketWaiting, 12, 0}, {14, "13800001014", "朱", "女士", model.TicketWaiting, 11, 0}, {15, "13800001015", "高", "先生", model.TicketWaiting, 10, 0}, {16, "13800001016", "林", "女士", model.TicketWaiting, 9, 0}, {17, "13800001017", "何", "先生", model.TicketWaiting, 8, 0}, {18, "13800001018", "郭", "女士", model.TicketWaiting, 7, 0}, {19, "13800001019", "马", "先生", model.TicketWaiting, 6, 0}, {20, "13800001020", "罗", "女士", model.TicketWaiting, 5, 0}, {21, "13800001021", "梁", "游客", model.TicketCanceled, 4, 0},
|
||||
}},
|
||||
{Code: "RAFT", Name: "峡谷漂流", Status: model.ProjectPaused, Prefix: "B", ETAMode: model.ETAContinuous, DeviceMode: "FAILURE", DisplayToken: envOr("RAFT_DISPLAY_TOKEN", "demo-display-raft"), BatchSize: 4, Grace: 8, BatchInterval: 600, Buffer: 5, Rate: 1.5, SessionStatus: "PAUSED", Tickets: []ticketSeed{
|
||||
{Code: "RAFT", Name: "峡谷漂流", Status: model.ProjectPaused, Prefix: "B", ETAMode: model.ETAContinuous, DeviceMode: "FAILURE", DisplayToken: envOr("RAFT_DISPLAY_TOKEN", "demo-display-raft"), CallMode: model.CallModePeople, BatchSize: 4, MaxTicketCall: 20, DefaultPeopleCall: 8, MaxPeopleCall: 24, MinPartySize: 1, MaxPartySize: 6, Grace: 8, BatchInterval: 600, Buffer: 5, Rate: 1.5, SessionStatus: "PAUSED", Tickets: []ticketSeed{
|
||||
{1, "13900002001", "郑", "女士", model.TicketWaiting, 35, 0}, {2, "13900002002", "谢", "先生", model.TicketWaiting, 30, 0}, {3, "13900002003", "宋", "女士", model.TicketWaiting, 25, 0}, {4, "13900002004", "唐", "先生", model.TicketWaiting, 20, 0}, {5, "13900002005", "许", "女士", model.TicketWaiting, 15, 0}, {6, "13900002006", "韩", "先生", model.TicketWaiting, 10, 0},
|
||||
}},
|
||||
{Code: "CABLE", Name: "云顶索道", Status: model.ProjectNotOpen, Prefix: "C", ETAMode: model.ETAFixedBatch, DeviceMode: "DISABLED", DisplayToken: envOr("CABLE_DISPLAY_TOKEN", "demo-display-cable"), BatchSize: 8, Grace: 5, BatchInterval: 720, Buffer: 5, Rate: 2, SessionStatus: ""},
|
||||
{Code: "CABLE", Name: "云顶索道", Status: model.ProjectNotOpen, Prefix: "C", ETAMode: model.ETAFixedBatch, DeviceMode: "DISABLED", DisplayToken: envOr("CABLE_DISPLAY_TOKEN", "demo-display-cable"), CallMode: model.CallModeTicket, BatchSize: 8, MaxTicketCall: 16, DefaultPeopleCall: 8, MaxPeopleCall: 32, MinPartySize: 1, MaxPartySize: 4, Grace: 5, BatchInterval: 720, Buffer: 5, Rate: 2, SessionStatus: ""},
|
||||
}
|
||||
if strings.EqualFold(envOr("SEED_MODE", "demo"), "smoke") {
|
||||
projects[0].Tickets = nil
|
||||
@@ -176,7 +178,7 @@ func resetProject(tx *gorm.DB, spec projectSeed, now time.Time) (model.Project,
|
||||
"DELETE FROM idempotency_keys WHERE project_id = ?", "DELETE FROM audit_entries WHERE project_id = ?",
|
||||
"DELETE FROM device_simulations WHERE project_id = ?", "DELETE FROM call_batch_tickets WHERE project_id = ?",
|
||||
"DELETE FROM call_batches WHERE project_id = ?", "DELETE FROM queue_tickets WHERE project_id = ?",
|
||||
"DELETE FROM queue_sessions WHERE project_id = ?", "DELETE FROM user_projects WHERE project_id = ?",
|
||||
"DELETE FROM queue_sessions WHERE project_id = ?",
|
||||
} {
|
||||
if err := tx.Exec(statement, project.ID).Error; err != nil {
|
||||
return project, err
|
||||
@@ -185,6 +187,9 @@ func resetProject(tx *gorm.DB, spec projectSeed, now time.Time) (model.Project,
|
||||
hash := security.HashToken(spec.DisplayToken)
|
||||
project.Code, project.Name, project.Status, project.Timezone, project.TicketPrefix = spec.Code, spec.Name, spec.Status, "Asia/Shanghai", spec.Prefix
|
||||
project.CallBatchSize, project.GracePeriodMinutes, project.ETAMode = spec.BatchSize, spec.Grace, spec.ETAMode
|
||||
project.CallMode, project.MaxCallTicketCount = spec.CallMode, spec.MaxTicketCall
|
||||
project.DefaultCallPeopleCount, project.MaxCallPeopleCount = spec.DefaultPeopleCall, spec.MaxPeopleCall
|
||||
project.MinPartySize, project.MaxPartySize = spec.MinPartySize, spec.MaxPartySize
|
||||
project.AverageBatchIntervalSeconds, project.ContinuousRatePerMinute, project.ETABufferMinutes = spec.BatchInterval, spec.Rate, spec.Buffer
|
||||
if spec.ETAMode == model.ETAContinuous {
|
||||
project.ETAIntervalSeconds = max(1, int(60/spec.Rate))
|
||||
@@ -192,7 +197,13 @@ func resetProject(tx *gorm.DB, spec projectSeed, now time.Time) (model.Project,
|
||||
project.ETAIntervalSeconds = max(1, spec.BatchInterval/spec.BatchSize)
|
||||
}
|
||||
project.VisitorNotice = model.DefaultVisitorNotice
|
||||
project.DisplayTokenHash, project.DeviceSimulationMode, project.UpdatedAt = &hash, spec.DeviceMode, now
|
||||
// Existing display bindings are operational configuration and survive a
|
||||
// queue-data rebuild. Newly created demo projects still receive the stable
|
||||
// seed token so local and smoke environments remain reproducible.
|
||||
if err == gorm.ErrRecordNotFound || project.DisplayTokenHash == nil {
|
||||
project.DisplayTokenHash = &hash
|
||||
}
|
||||
project.DeviceSimulationMode, project.UpdatedAt = spec.DeviceMode, now
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
err = tx.Create(&project).Error
|
||||
} else {
|
||||
@@ -214,6 +225,7 @@ func seedQueue(tx *gorm.DB, cipher *security.Cipher, project model.Project, crea
|
||||
}
|
||||
byBatch := map[int][]model.QueueTicket{}
|
||||
visitors := map[string]string{}
|
||||
totalPeople := 0
|
||||
for _, item := range spec.Tickets {
|
||||
joined := now.Add(-time.Duration(item.MinutesAgo) * time.Minute)
|
||||
token := fmt.Sprintf("demo-visitor-%s-%03d", strings.ToLower(project.Code), item.Number)
|
||||
@@ -230,7 +242,9 @@ func seedQueue(tx *gorm.DB, cipher *security.Cipher, project model.Project, crea
|
||||
return nil, err
|
||||
}
|
||||
phoneDigest := cipher.Digest(item.Phone)
|
||||
ticket := model.QueueTicket{ID: uuid.NewString(), ProjectID: project.ID, QueueSessionID: session.ID, TicketNumber: item.Number, DisplayNumber: displayNumber, PublicTokenHash: security.HashToken(token), PhoneCiphertext: phoneCipher, PhoneNonce: phoneNonce, PhoneHMAC: &phoneDigest, LastNameCiphertext: lastCipher, LastNameNonce: lastNonce, Honorific: item.Honorific, Status: item.Status, JoinedAt: joined, PersonalDataPurgeAt: now.Add(30 * 24 * time.Hour), CreatedBy: creator, CreatedAt: joined, UpdatedAt: now}
|
||||
partySize := spec.MinPartySize + (item.Number-1)%(spec.MaxPartySize-spec.MinPartySize+1)
|
||||
totalPeople += partySize
|
||||
ticket := model.QueueTicket{ID: uuid.NewString(), ProjectID: project.ID, QueueSessionID: session.ID, TicketNumber: item.Number, DisplayNumber: displayNumber, PartySize: partySize, PublicTokenHash: security.HashToken(token), PhoneCiphertext: phoneCipher, PhoneNonce: phoneNonce, PhoneHMAC: &phoneDigest, LastNameCiphertext: lastCipher, LastNameNonce: lastNonce, Honorific: item.Honorific, Status: item.Status, JoinedAt: joined, PersonalDataPurgeAt: now.Add(30 * 24 * time.Hour), CreatedBy: creator, CreatedAt: joined, UpdatedAt: now}
|
||||
called := joined.Add(8 * time.Minute)
|
||||
arrived := called.Add(3 * time.Minute)
|
||||
completed := called.Add(8 * time.Minute)
|
||||
@@ -271,7 +285,15 @@ func seedQueue(tx *gorm.DB, cipher *security.Cipher, project model.Project, crea
|
||||
done := calledAt.Add(12 * time.Minute)
|
||||
completedAt = &done
|
||||
}
|
||||
batch := model.CallBatch{ID: uuid.NewString(), ProjectID: project.ID, QueueSessionID: session.ID, BatchSequence: sequence, Revision: int64(sequence), Status: status, RequestedBy: creator, CalledAt: calledAt, CompletedAt: completedAt, CreatedAt: calledAt}
|
||||
peopleCount := 0
|
||||
for _, ticket := range tickets {
|
||||
peopleCount += ticket.PartySize
|
||||
}
|
||||
callMode, requestedCount := model.CallModeTicket, len(tickets)
|
||||
if project.CallMode == model.CallModePeople {
|
||||
callMode, requestedCount = model.CallModePeople, peopleCount
|
||||
}
|
||||
batch := model.CallBatch{ID: uuid.NewString(), ProjectID: project.ID, QueueSessionID: session.ID, BatchSequence: sequence, Revision: int64(sequence), Status: status, CallMode: callMode, RequestedCount: requestedCount, TicketCount: len(tickets), PeopleCount: peopleCount, RequestedBy: creator, CalledAt: calledAt, CompletedAt: completedAt, CreatedAt: calledAt}
|
||||
if err := tx.Create(&batch).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -291,7 +313,7 @@ func seedQueue(tx *gorm.DB, cipher *security.Cipher, project model.Project, crea
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
details, _ := json.Marshal(map[string]any{"source": "demo_seed", "ticket_count": len(spec.Tickets)})
|
||||
details, _ := json.Marshal(map[string]any{"source": "demo_seed", "ticket_count": len(spec.Tickets), "people_count": totalPeople})
|
||||
projectID, actorID := project.ID, creator
|
||||
audit := model.AuditEntry{ID: uuid.NewString(), ProjectID: &projectID, ActorUserID: &actorID, Action: "DEMO_DATA_SEEDED", EntityType: "PROJECT", EntityID: &projectID, Details: details, RequestID: "seed", UserAgent: "cmd/seed", RetainUntil: now.AddDate(1, 0, 0), CreatedAt: now}
|
||||
if err := tx.Create(&audit).Error; err != nil {
|
||||
|
||||
Reference in New Issue
Block a user