修复投票计数与状态同步,完善票据与戳显示
修复投票系统:禁止重复投票、恢复状态、同步大屏 完善投票流程与展示:计数准确、状态可恢复、样式统一
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
-- 1. Each user has 7 distinct tickets (creative, visual, etc.)
|
||||
-- 2. Each ticket can only be assigned to ONE program
|
||||
-- 3. A user can only give ONE ticket to each program (no multi-ticket to same program)
|
||||
-- 4. Supports revoke: if user already used this ticket, revoke old vote first
|
||||
-- 4. 禁止同一用户在同一奖项重复投票(不允许改投)
|
||||
--
|
||||
-- KEYS[1] = vote:user:{userId}:tickets (Hash)
|
||||
-- KEYS[2] = vote:user:{userId}:programs (Set)
|
||||
@@ -49,15 +49,13 @@ end
|
||||
local already_voted_program = redis.call('SISMEMBER', user_programs_key, program_id)
|
||||
local current_ticket_program = redis.call('HGET', user_tickets_key, ticket_type)
|
||||
|
||||
-- Case: User trying to vote same program with same ticket (no-op)
|
||||
if current_ticket_program == program_id then
|
||||
-- Case: User already used this ticket (no re-vote allowed)
|
||||
if current_ticket_program and current_ticket_program ~= false then
|
||||
redis.call('DEL', lock_key)
|
||||
return cjson.encode({
|
||||
success = true,
|
||||
message = 'Already voted for this program with this ticket',
|
||||
program_id = program_id,
|
||||
ticket_type = ticket_type,
|
||||
is_duplicate = true
|
||||
success = false,
|
||||
error = 'ALREADY_VOTED',
|
||||
message = 'You already used this ticket'
|
||||
})
|
||||
end
|
||||
|
||||
@@ -71,25 +69,7 @@ if already_voted_program == 1 and current_ticket_program ~= program_id then
|
||||
})
|
||||
end
|
||||
|
||||
-- Step 3: If this ticket was used before, revoke the old vote
|
||||
local old_program_id = current_ticket_program
|
||||
local revoked = false
|
||||
|
||||
if old_program_id and old_program_id ~= false then
|
||||
-- Decrement old program's count
|
||||
local old_count_key = 'vote:count:' .. old_program_id .. ':' .. ticket_type
|
||||
local old_leaderboard_key = 'vote:leaderboard:' .. ticket_type
|
||||
local old_voters_key = 'vote:program:' .. old_program_id .. ':voters'
|
||||
|
||||
redis.call('DECR', old_count_key)
|
||||
redis.call('ZINCRBY', old_leaderboard_key, -1, old_program_id)
|
||||
redis.call('SREM', old_voters_key, user_id)
|
||||
redis.call('SREM', user_programs_key, old_program_id)
|
||||
|
||||
revoked = true
|
||||
end
|
||||
|
||||
-- Step 4: Cast the new vote
|
||||
-- Step 3: Cast the new vote
|
||||
-- 4a: Set the ticket assignment
|
||||
redis.call('HSET', user_tickets_key, ticket_type, program_id)
|
||||
|
||||
@@ -111,8 +91,7 @@ local vote_record = cjson.encode({
|
||||
user_id = user_id,
|
||||
program_id = program_id,
|
||||
ticket_type = ticket_type,
|
||||
timestamp = timestamp,
|
||||
revoked_program = old_program_id or nil
|
||||
timestamp = timestamp
|
||||
})
|
||||
redis.call('RPUSH', sync_queue_key, vote_record)
|
||||
|
||||
@@ -123,7 +102,5 @@ return cjson.encode({
|
||||
success = true,
|
||||
program_id = program_id,
|
||||
ticket_type = ticket_type,
|
||||
new_count = new_count,
|
||||
revoked = revoked,
|
||||
revoked_program = old_program_id or nil
|
||||
new_count = new_count
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
-- KEYS[4] = leaderboard:{category}
|
||||
-- KEYS[5] = sync:queue:votes
|
||||
-- KEYS[6] = lock:vote:{user_id}:{category}
|
||||
-- KEYS[7] = vote:user:{userId}:tickets (Hash)
|
||||
--
|
||||
-- ARGV[1] = candidate_id
|
||||
-- ARGV[2] = user_id
|
||||
@@ -22,6 +23,7 @@ local category_voters_key = KEYS[3]
|
||||
local leaderboard_key = KEYS[4]
|
||||
local sync_queue_key = KEYS[5]
|
||||
local lock_key = KEYS[6]
|
||||
local user_tickets_key = KEYS[7]
|
||||
|
||||
local candidate_id = ARGV[1]
|
||||
local user_id = ARGV[2]
|
||||
@@ -70,6 +72,9 @@ local new_count = redis.call('HINCRBY', vote_count_key, candidate_id, 1)
|
||||
-- 4b: Add category to user's voted categories
|
||||
redis.call('SADD', user_categories_key, category)
|
||||
|
||||
-- 4b-2: Persist user's choice for this category
|
||||
redis.call('HSET', user_tickets_key, category, candidate_id)
|
||||
|
||||
-- 4c: Add user to category's voters
|
||||
redis.call('SADD', category_voters_key, user_id)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import type {
|
||||
VoteStamp,
|
||||
} from '@gala/shared/types';
|
||||
import { INITIAL_ADMIN_STATE, PRIZE_CONFIG } from '@gala/shared/types';
|
||||
import { VOTE_KEYS, TICKET_TYPES } from '@gala/shared/constants';
|
||||
|
||||
const ADMIN_STATE_KEY = 'gala:admin:state';
|
||||
|
||||
@@ -39,6 +40,17 @@ class AdminService extends EventEmitter {
|
||||
const savedState = await redis.get(ADMIN_STATE_KEY);
|
||||
if (savedState) {
|
||||
const parsed = JSON.parse(savedState);
|
||||
const configPrograms = programConfigService.getVotingPrograms();
|
||||
const savedPrograms = (parsed?.voting?.programs || []) as VotingProgram[];
|
||||
const mergedPrograms = configPrograms.map((program) => {
|
||||
const saved = savedPrograms.find(p => p.id === program.id);
|
||||
return {
|
||||
...program,
|
||||
status: saved?.status ?? program.status,
|
||||
votes: saved?.votes ?? 0,
|
||||
stamps: saved?.stamps ?? [],
|
||||
};
|
||||
});
|
||||
// Deep merge to ensure new fields have defaults
|
||||
this.state = {
|
||||
...INITIAL_ADMIN_STATE,
|
||||
@@ -46,10 +58,9 @@ class AdminService extends EventEmitter {
|
||||
voting: {
|
||||
...INITIAL_ADMIN_STATE.voting,
|
||||
...parsed.voting,
|
||||
// Ensure programs always has default values from config service
|
||||
programs: parsed.voting?.programs?.length > 0
|
||||
? parsed.voting.programs
|
||||
: programConfigService.getVotingPrograms(),
|
||||
// 始终从配置服务加载最新的 programs 和 awards
|
||||
programs: mergedPrograms,
|
||||
awards: programConfigService.getAwards(),
|
||||
},
|
||||
lottery: {
|
||||
...INITIAL_ADMIN_STATE.lottery,
|
||||
@@ -62,10 +73,24 @@ class AdminService extends EventEmitter {
|
||||
};
|
||||
logger.info('Admin state restored from Redis (merged with defaults)');
|
||||
} else {
|
||||
const programSettings = programConfigService.getSettings();
|
||||
this.state = {
|
||||
...INITIAL_ADMIN_STATE,
|
||||
voting: {
|
||||
...INITIAL_ADMIN_STATE.voting,
|
||||
programs: programConfigService.getVotingPrograms(),
|
||||
awards: programConfigService.getAwards(),
|
||||
allowLateCatch: programSettings.allowLateCatch,
|
||||
},
|
||||
};
|
||||
await this.saveState();
|
||||
logger.info('Admin state initialized with defaults');
|
||||
}
|
||||
|
||||
// Sync actual vote counts from Redis (VotingEngine is source of truth)
|
||||
await this.syncVotesFromRedis();
|
||||
|
||||
|
||||
// 从配置文件刷新当前轮次的奖项信息
|
||||
await this.refreshPrizeFromConfig();
|
||||
} catch (error) {
|
||||
@@ -297,12 +322,30 @@ class AdminService extends EventEmitter {
|
||||
/**
|
||||
* Add a vote stamp to a program (for display on big screen)
|
||||
*/
|
||||
async addVoteStamp(programId: string, userName: string, department: string, ticketType: string): Promise<{ success: boolean; stamp?: VoteStamp }> {
|
||||
async addVoteStamp(
|
||||
programId: string,
|
||||
userName: string,
|
||||
department: string,
|
||||
ticketType: string,
|
||||
options?: { revokedProgramId?: string }
|
||||
): Promise<{ success: boolean; stamp?: VoteStamp; programVotes?: number; totalVotes?: number; revokedProgramId?: string; revokedProgramVotes?: number }> {
|
||||
const program = this.state.voting.programs.find(p => p.id === programId);
|
||||
if (!program) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
let wasReplacement = false;
|
||||
let revokedProgramVotes: number | undefined;
|
||||
const revokedProgramId = options?.revokedProgramId;
|
||||
if (revokedProgramId && revokedProgramId !== programId) {
|
||||
const revokedProgram = this.state.voting.programs.find(p => p.id === revokedProgramId);
|
||||
if (revokedProgram && revokedProgram.votes > 0) {
|
||||
revokedProgram.votes -= 1;
|
||||
revokedProgramVotes = revokedProgram.votes;
|
||||
wasReplacement = true;
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const dateStr = `${now.getFullYear()}.${String(now.getMonth() + 1).padStart(2, '0')}.${String(now.getDate()).padStart(2, '0')}`;
|
||||
|
||||
@@ -321,10 +364,19 @@ class AdminService extends EventEmitter {
|
||||
if (!program.stamps) program.stamps = [];
|
||||
program.stamps.push(stamp);
|
||||
program.votes++;
|
||||
this.state.voting.totalVotes++;
|
||||
if (!wasReplacement) {
|
||||
this.state.voting.totalVotes++;
|
||||
}
|
||||
|
||||
await this.saveState();
|
||||
return { success: true, stamp };
|
||||
return {
|
||||
success: true,
|
||||
stamp,
|
||||
programVotes: program.votes,
|
||||
totalVotes: this.state.voting.totalVotes,
|
||||
revokedProgramId,
|
||||
revokedProgramVotes,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -501,12 +553,15 @@ class AdminService extends EventEmitter {
|
||||
|
||||
try {
|
||||
if (scope === 'all' || scope === 'voting') {
|
||||
const programSettings = programConfigService.getSettings();
|
||||
this.state.voting = {
|
||||
...INITIAL_ADMIN_STATE.voting,
|
||||
programs: programConfigService.getVotingPrograms(),
|
||||
awards: programConfigService.getAwards(),
|
||||
allowLateCatch: programSettings.allowLateCatch,
|
||||
};
|
||||
// Clear voting data in Redis
|
||||
await redis.del('gala:votes:*');
|
||||
await this.clearVotingRedisData();
|
||||
}
|
||||
|
||||
if (scope === 'all' || scope === 'lottery') {
|
||||
@@ -577,6 +632,66 @@ class AdminService extends EventEmitter {
|
||||
this.state.voting.totalVotes = count;
|
||||
await this.saveState();
|
||||
}
|
||||
/**
|
||||
* Sync vote counts from Redis (source of truth) to local state
|
||||
*/
|
||||
async syncVotesFromRedis(): Promise<void> {
|
||||
try {
|
||||
let total = 0;
|
||||
for (const program of this.state.voting.programs) {
|
||||
let count = 0;
|
||||
for (const ticketType of TICKET_TYPES) {
|
||||
const key = VOTE_KEYS.count(program.id, ticketType);
|
||||
const value = await redis.get(key);
|
||||
if (value) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(parsed)) {
|
||||
count += parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
program.votes = count;
|
||||
total += count;
|
||||
}
|
||||
this.state.voting.totalVotes = total;
|
||||
|
||||
// We don't save state here immediately to avoid overwriting other potential changes
|
||||
// during init, but since we called this in initialize(), we should save.
|
||||
await this.saveState();
|
||||
logger.info({ totalVotes: total }, 'Synced vote counts from Redis');
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Failed to sync votes from Redis');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear voting-related Redis keys (both new ticket system and legacy)
|
||||
*/
|
||||
private async clearVotingRedisData(): Promise<void> {
|
||||
const patterns = [
|
||||
'vote:count:*',
|
||||
'vote:user:*',
|
||||
'vote:program:*',
|
||||
'vote:leaderboard:*',
|
||||
'vote:category:*',
|
||||
'vote:sync:queue',
|
||||
'sync:queue:votes',
|
||||
'vote:lock:*',
|
||||
'lock:vote:*',
|
||||
'leaderboard:*',
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
let cursor = '0';
|
||||
do {
|
||||
const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 1000);
|
||||
cursor = nextCursor;
|
||||
if (keys.length > 0) {
|
||||
await redis.del(...keys);
|
||||
}
|
||||
} while (cursor !== '0');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const adminService = new AdminService();
|
||||
|
||||
@@ -18,6 +18,15 @@ export interface ProgramConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
teamName: string;
|
||||
performer?: string; // 表演者
|
||||
order: number;
|
||||
remark?: string; // 节目备注
|
||||
}
|
||||
|
||||
export interface AwardConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
@@ -28,6 +37,7 @@ export interface ProgramSettings {
|
||||
|
||||
export interface ProgramConfigFile {
|
||||
programs: ProgramConfig[];
|
||||
awards: AwardConfig[];
|
||||
settings: ProgramSettings;
|
||||
}
|
||||
|
||||
@@ -49,8 +59,19 @@ class ProgramConfigService {
|
||||
this.config = JSON.parse(content);
|
||||
logger.info({
|
||||
programCount: this.config?.programs.length,
|
||||
awardCount: this.config?.awards?.length || 0,
|
||||
configPath: this.configPath
|
||||
}, 'Program config loaded');
|
||||
|
||||
// Validate: programs.length === awards.length
|
||||
if (this.config?.programs && this.config?.awards) {
|
||||
if (this.config.programs.length !== this.config.awards.length) {
|
||||
logger.warn({
|
||||
programCount: this.config.programs.length,
|
||||
awardCount: this.config.awards.length
|
||||
}, 'Warning: program count does not match award count');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.warn({ configPath: this.configPath }, 'Program config file not found, using defaults');
|
||||
this.config = this.getDefaults();
|
||||
@@ -67,14 +88,22 @@ class ProgramConfigService {
|
||||
private getDefaults(): ProgramConfigFile {
|
||||
return {
|
||||
programs: [
|
||||
{ id: 'p1', name: '龙腾四海', teamName: '市场部', order: 1 },
|
||||
{ id: 'p2', name: '金马奔腾', teamName: '技术部', order: 2 },
|
||||
{ id: 'p3', name: '春风得意', teamName: '人力资源部', order: 3 },
|
||||
{ id: 'p4', name: '鸿运当头', teamName: '财务部', order: 4 },
|
||||
{ id: 'p5', name: '马到成功', teamName: '运营部', order: 5 },
|
||||
{ id: 'p6', name: '一马当先', teamName: '产品部', order: 6 },
|
||||
{ id: 'p7', name: '万马奔腾', teamName: '设计部', order: 7 },
|
||||
{ id: 'p8', name: '龙马精神', teamName: '销售部', order: 8 },
|
||||
{ id: 'p1', name: '龙腾四海', teamName: '市场部', performer: '待定', order: 1, remark: '赞美节目如琥珀般凝固了某个经典、美好、闪光的瞬间。' },
|
||||
{ id: 'p2', name: '金马奔腾', teamName: '技术部', performer: '待定', order: 2, remark: '强调节目留下了值得回味的"声音"。' },
|
||||
{ id: 'p3', name: '春风得意', teamName: '人力资源部', performer: '待定', order: 3, remark: '赞美节目引发了跨越时代的共鸣。' },
|
||||
{ id: 'p4', name: '鸿运当头', teamName: '财务部', performer: '待定', order: 4, remark: '形容节目用声音和表演编织了一个时代的梦境。' },
|
||||
{ id: 'p5', name: '马到成功', teamName: '运营部', performer: '待定', order: 5, remark: '既指复刻了过去的潮流,也指创造了今晚的潮流。' },
|
||||
{ id: 'p6', name: '一马当先', teamName: '产品部', performer: '待定', order: 6, remark: '强调节目的独特韵味与精心打磨。' },
|
||||
{ id: 'p7', name: '万马奔腾', teamName: '设计部', performer: '待定', order: 7, remark: '赞美节目与"复古70-80"主题高度契合。' },
|
||||
],
|
||||
awards: [
|
||||
{ id: 'time_amber', name: '时光琥珀奖', icon: '🏆', order: 1 },
|
||||
{ id: 'echo_years', name: '岁月留声奖', icon: '🎵', order: 2 },
|
||||
{ id: 'resonance', name: '风华共鸣奖', icon: '🎭', order: 3 },
|
||||
{ id: 'dream_weaver', name: '光影织梦奖', icon: '✨', order: 4 },
|
||||
{ id: 'trend_mark', name: '潮流印记奖', icon: '🌊', order: 5 },
|
||||
{ id: 'craftsmanship', name: '匠心独韵奖', icon: '💎', order: 6 },
|
||||
{ id: 'in_sync', name: '同频时代奖', icon: '📻', order: 7 },
|
||||
],
|
||||
settings: {
|
||||
allowLateCatch: true,
|
||||
@@ -90,6 +119,20 @@ class ProgramConfigService {
|
||||
return this.config?.programs || this.getDefaults().programs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all awards
|
||||
*/
|
||||
getAwards(): AwardConfig[] {
|
||||
return this.config?.awards || this.getDefaults().awards;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get award by id
|
||||
*/
|
||||
getAwardById(id: string): AwardConfig | undefined {
|
||||
return this.getAwards().find(a => a.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert config programs to VotingProgram format (with runtime fields)
|
||||
*/
|
||||
@@ -135,6 +178,24 @@ class ProgramConfigService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update awards and save to file
|
||||
*/
|
||||
async updateAwards(awards: AwardConfig[]): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
if (!this.config) {
|
||||
this.config = this.getDefaults();
|
||||
}
|
||||
this.config.awards = awards;
|
||||
await this.saveToFile();
|
||||
logger.info({ awardCount: awards.length }, 'Awards updated');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Failed to update awards');
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update settings and save to file
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { redis } from '../config/redis';
|
||||
import { config } from '../config';
|
||||
import { logger } from '../utils/logger';
|
||||
import { REDIS_KEYS } from '@gala/shared/constants';
|
||||
import { REDIS_KEYS, VOTE_KEYS } from '@gala/shared/constants';
|
||||
import type { VoteCategory } from '@gala/shared/types';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
@@ -85,6 +85,7 @@ export class VoteService {
|
||||
`${REDIS_KEYS.LEADERBOARD}:${category}`,
|
||||
REDIS_KEYS.SYNC_QUEUE,
|
||||
`${REDIS_KEYS.VOTE_LOCK}:${userId}:${category}`,
|
||||
VOTE_KEYS.userTickets(userId),
|
||||
];
|
||||
|
||||
const args = [
|
||||
|
||||
@@ -7,7 +7,7 @@ import { logger } from '../utils/logger';
|
||||
import { voteService } from '../services/vote.service';
|
||||
import { votingEngine } from '../services/voting.engine';
|
||||
import { adminService } from '../services/admin.service';
|
||||
import { SOCKET_EVENTS, SOCKET_ROOMS, TICKET_TYPES, type TicketType } from '@gala/shared/constants';
|
||||
import { SOCKET_EVENTS, SOCKET_ROOMS, TICKET_TYPES, VOTE_KEYS, type TicketType } from '@gala/shared/constants';
|
||||
import type {
|
||||
ServerToClientEvents,
|
||||
ClientToServerEvents,
|
||||
@@ -206,8 +206,9 @@ async function handleJoin(
|
||||
await socket.join(SOCKET_ROOMS.ADMIN);
|
||||
}
|
||||
|
||||
// Get user's voted categories
|
||||
// Get user's voted categories and tickets
|
||||
const votedCategories = await voteService.getUserVotedCategories(userId);
|
||||
const userTickets = await redis.hgetall(VOTE_KEYS.userTickets(userId));
|
||||
|
||||
logger.info({ socketId: socket.id, userId, userName, role }, 'User joined');
|
||||
|
||||
@@ -221,6 +222,8 @@ async function handleJoin(
|
||||
sessionId: socket.id,
|
||||
serverTime: Date.now(),
|
||||
reconnected: false,
|
||||
votedCategories,
|
||||
userTickets,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -298,18 +301,30 @@ async function handleVoteSubmit(
|
||||
data.candidateId,
|
||||
socket.data.userName || '匿名用户',
|
||||
socket.data.department || '未知部门',
|
||||
category
|
||||
category,
|
||||
{ revokedProgramId: result.revoked_program }
|
||||
);
|
||||
|
||||
// Broadcast vote update to all clients with stamp info
|
||||
io.to(SOCKET_ROOMS.ALL).emit(SOCKET_EVENTS.VOTE_UPDATED as any, {
|
||||
candidateId: data.candidateId,
|
||||
category: category,
|
||||
totalVotes: result.new_count!,
|
||||
totalVotes: stampResult.totalVotes ?? 0,
|
||||
programVotes: stampResult.programVotes ?? 0,
|
||||
delta: 1,
|
||||
stamp: stampResult.stamp, // Pass the stamp info for animation
|
||||
});
|
||||
|
||||
if (stampResult.revokedProgramId && stampResult.revokedProgramVotes !== undefined) {
|
||||
io.to(SOCKET_ROOMS.ALL).emit(SOCKET_EVENTS.VOTE_UPDATED as any, {
|
||||
candidateId: stampResult.revokedProgramId,
|
||||
category: category,
|
||||
totalVotes: stampResult.totalVotes ?? 0,
|
||||
programVotes: stampResult.revokedProgramVotes,
|
||||
delta: -1,
|
||||
});
|
||||
}
|
||||
|
||||
safeCallback({
|
||||
success: true,
|
||||
data: {
|
||||
@@ -346,7 +361,8 @@ async function handleVoteSubmit(
|
||||
io.to(SOCKET_ROOMS.ALL).emit(SOCKET_EVENTS.VOTE_UPDATED as any, {
|
||||
candidateId: data.candidateId,
|
||||
category: data.category,
|
||||
totalVotes: result.new_count!,
|
||||
totalVotes: stampResult.totalVotes ?? 0,
|
||||
programVotes: stampResult.programVotes ?? 0,
|
||||
delta: 1,
|
||||
stamp: stampResult.stamp, // Include stamp for big screen
|
||||
});
|
||||
@@ -380,10 +396,12 @@ async function handleSyncRequest(socket: GalaSocket): Promise<void> {
|
||||
|
||||
try {
|
||||
const votedCategories = await voteService.getUserVotedCategories(userId);
|
||||
const userTickets = await redis.hgetall(VOTE_KEYS.userTickets(userId));
|
||||
|
||||
socket.emit(SOCKET_EVENTS.SYNC_STATE as any, {
|
||||
votes: {}, // TODO: Include current vote counts
|
||||
userVotedCategories: votedCategories,
|
||||
userTickets,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error({ socketId: socket.id, userId, error }, 'Sync request error');
|
||||
@@ -649,4 +667,3 @@ function handleScanUnsubscribe(socket: GalaSocket, data: ScanSubscribePayload):
|
||||
socket.leave(roomName);
|
||||
logger.info({ socketId: socket.id, scanToken }, 'Socket unsubscribed from scan updates');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user