feat: enhance lottery system with participant import and prize config

- Fix ES module import issue in admin.service.ts (require -> import)
- Fix lottery reveal ghosting by hiding name particles on complete
- Add participant import from Excel with tag calculation
- Add prize configuration service with JSON persistence
- Constrain winners overlay to scroll area dimensions
- Fix macOS lsof syntax in stop script
- Add HorseRace view and renderer (WIP)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
empty
2026-01-23 12:20:45 +08:00
parent 35d77cbb22
commit a442d050e4
23 changed files with 2523 additions and 325 deletions

View File

@@ -1,6 +1,10 @@
import { Router, IRouter } from 'express';
import multer from 'multer';
import { participantService } from '../services/participant.service';
import { prizeConfigService } from '../services/prize-config.service';
const router: IRouter = Router();
const upload = multer({ storage: multer.memoryStorage() });
/**
* GET /api/admin/stats
@@ -12,7 +16,7 @@ router.get('/stats', async (_req, res, next) => {
return res.json({
success: true,
data: {
totalUsers: 0,
totalUsers: participantService.getCount(),
totalVotes: 0,
activeConnections: 0,
},
@@ -22,6 +26,53 @@ router.get('/stats', async (_req, res, next) => {
}
});
/**
* GET /api/admin/prizes
* Get prize configuration
*/
router.get('/prizes', async (_req, res, next) => {
try {
const config = prizeConfigService.getFullConfig();
return res.json({
success: true,
data: config,
});
} catch (error) {
next(error);
}
});
/**
* PUT /api/admin/prizes
* Update prize configuration
*/
router.put('/prizes', async (req, res, next) => {
try {
const { prizes, settings } = req.body;
if (prizes) {
const result = await prizeConfigService.updatePrizes(prizes);
if (!result.success) {
return res.status(400).json({ success: false, error: result.error });
}
}
if (settings) {
const result = await prizeConfigService.updateSettings(settings);
if (!result.success) {
return res.status(400).json({ success: false, error: result.error });
}
}
return res.json({
success: true,
data: prizeConfigService.getFullConfig(),
});
} catch (error) {
next(error);
}
});
/**
* POST /api/admin/draw/start
* Start a lucky draw
@@ -54,4 +105,54 @@ router.post('/draw/stop', async (_req, res, next) => {
}
});
/**
* POST /api/admin/participants/import
* Import participants from Excel file
* Expected columns: 岗位, 姓名, 年份
*/
router.post('/participants/import', upload.single('file'), async (req, res, next) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
error: '请上传 Excel 文件',
});
}
const result = await participantService.importFromExcel(req.file.buffer);
return res.json({
success: result.success,
data: {
totalCount: result.totalCount,
importedCount: result.importedCount,
tagDistribution: result.tagDistribution,
errors: result.errors,
},
});
} catch (error) {
next(error);
}
});
/**
* GET /api/admin/participants
* Get all participants
*/
router.get('/participants', async (_req, res, next) => {
try {
const participants = participantService.getAll();
return res.json({
success: true,
data: {
count: participants.length,
participants,
},
});
} catch (error) {
next(error);
}
});
export default router;