feat: 优化100人并发投票支持 + 修复奖项名称硬编码问题

服务端优化:
- Socket.IO 配置优化,支持高并发 WebSocket 连接
- 添加 connectTimeout、perMessageDeflate 等参数

压测脚本:
- 新增 vote-real-scenario.yaml 真实场景压测配置
- 支持 100人瞬间爆发、每人7票、不同投票速度模拟
- 添加随机延迟函数模拟真实用户行为
- 强制 WebSocket 传输避免 HTTP 连接限制

前端修复:
- 修复 PostcardItem、PostcardDisplay 奖项名称硬编码问题
- 组件现在从后端 awards 配置动态获取奖项名称
- 修复 LiveVotingView、AdminControl 传递 awards 数据
- 新增 gala 压测命令到 package.json

测试验证:
- 100人并发压测通过,成功率 100%
- P95 延迟 0.7ms,远低于 500ms 阈值
- 系统可稳定支持 3.3 倍现场负载
This commit is contained in:
empty
2026-01-29 00:09:03 +08:00
parent 2f8bf0d755
commit 5c5d0ad85c
10 changed files with 329 additions and 89 deletions

View File

@@ -1,19 +1,8 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { VotingProgram, VoteStamp } from '@gala/shared/types';
import type { VotingProgram, VoteStamp, AwardConfig } from '@gala/shared/types';
import Postmark from './Postmark.vue';
// 票据类型名称映射
const TICKET_TYPE_NAMES: Record<string, string> = {
creative: '最佳创意',
visual: '最佳视觉',
atmosphere: '最佳氛围',
performance: '最佳表演',
teamwork: '最佳团队',
popularity: '最受欢迎',
potential: '最具潜力',
};
interface Props {
program: VotingProgram;
isFocused: boolean;
@@ -24,6 +13,7 @@ interface Props {
rotateZ?: number; // 3D 旋转 Z
z?: number; // 3D 位移 Z
index?: number; // 传入索引用于生成邮编
awards?: AwardConfig[]; // 奖项配置
}
const props = withDefaults(defineProps<Props>(), {
@@ -77,6 +67,15 @@ function getStampColor(ticketType: string): string {
};
return colors[ticketType] || '#e8313f';
}
// 从 awards 配置中查找奖项名称
function getAwardName(type: string): string {
if (!props.awards || props.awards.length === 0) {
return type;
}
const award = props.awards.find(a => a.id === type);
return award?.name || type;
}
</script>
<template>
@@ -158,7 +157,7 @@ function getStampColor(ticketType: string): string {
:style="getStampStyle(stamp)"
>
<Postmark
:award-name="TICKET_TYPE_NAMES[stamp.ticketType] || stamp.ticketType"
:award-name="getAwardName(stamp.ticketType)"
:user-name="stamp.userName"
color="red"
/>

View File

@@ -1,10 +1,11 @@
<script setup lang="ts">
import { computed, ref, onMounted, onUnmounted } from 'vue';
import PostcardItem from './PostcardItem.vue';
import type { VotingProgram } from '@gala/shared/types';
import type { VotingProgram, AwardConfig } from '@gala/shared/types';
export interface Props {
programs: VotingProgram[];
awards?: AwardConfig[];
columns?: number;
rows?: number;
}
@@ -65,6 +66,7 @@ defineExpose({
:order="program.order"
:votes="program.votes"
:stamps="program.stamps"
:awards="awards"
class="grid-item"
/>

View File

@@ -1,20 +1,9 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { VoteStamp } from '@gala/shared/types';
import type { VoteStamp, AwardConfig } from '@gala/shared/types';
import stampImage from '../assets/images/stamp-horse-2026.png';
import Postmark from './Postmark.vue';
// 票据类型名称映射
const TICKET_TYPE_NAMES: Record<string, string> = {
creative: '最佳创意',
visual: '最佳视觉',
atmosphere: '最佳氛围',
performance: '最佳表演',
teamwork: '最佳团队',
popularity: '最受欢迎',
potential: '最具潜力',
};
export interface Props {
id: string;
name: string;
@@ -24,6 +13,7 @@ export interface Props {
order: number;
votes: number;
stamps?: VoteStamp[];
awards?: AwardConfig[]; // 奖项配置(从后端获取)
}
const props = withDefaults(defineProps<Props>(), {
@@ -72,19 +62,14 @@ function getStampStyle(stamp: VoteStamp) {
// Stamp target ref for particle animation
const stampTargetRef = ref<HTMLElement | null>(null);
// Award name mapping for stamps
const awardNames: Record<string, string> = {
creative: '最佳创意奖',
visual: '最佳视觉奖',
atmosphere: '最佳气氛奖',
performance: '最佳表演奖',
teamwork: '最佳团队奖',
popularity: '最受欢迎奖',
potential: '最具潜力奖',
};
// 从 awards 配置中查找奖项名称
function getAwardName(type: string) {
return awardNames[type] || '优秀节目奖';
if (!props.awards || props.awards.length === 0) {
// 如果没有 awards 配置返回票种ID本身
return type;
}
const award = props.awards.find(a => a.id === type);
return award?.name || type;
}
defineExpose({

View File

@@ -334,24 +334,14 @@ const lotteryPhaseLabel = computed(() => {
}
});
// Award type labels
const awardLabels: Record<string, string> = {
creative: '最佳创意奖',
visual: '最佳视觉奖',
atmosphere: '最佳气氛奖',
performance: '最佳表演奖',
teamwork: '最佳团队奖',
popularity: '最受欢迎奖',
potential: '最具潜力奖',
};
// Compute award statistics grouped by award type
const awardStats = computed(() => {
const stats: Record<string, Array<{ programName: string; teamName: string; votes: number; programId: string }>> = {};
// Initialize all award types
Object.keys(awardLabels).forEach(type => {
stats[type] = [];
// Initialize all award types from admin.awards
const awards = admin.awards || [];
awards.forEach((award: any) => {
stats[award.id] = [];
});
// Aggregate stamps by award type

View File

@@ -3,7 +3,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { useDisplayStore } from '../stores/display';
import PostcardGrid from '../components/PostcardGrid.vue';
import type { VotingProgram, AdminState } from '@gala/shared/types';
import type { VotingProgram, AdminState, AwardConfig } from '@gala/shared/types';
import { SOCKET_EVENTS } from '@gala/shared/constants';
const router = useRouter();
@@ -11,6 +11,7 @@ const displayStore = useDisplayStore();
// 节目列表
const programs = ref<VotingProgram[]>([]);
const awards = ref<AwardConfig[]>([]);
const votingOpen = ref(false);
const totalVotes = ref(0);
@@ -29,6 +30,7 @@ function goBack() {
// 处理状态同步
function handleStateSync(state: AdminState) {
programs.value = state.voting.programs;
awards.value = state.voting.awards || [];
votingOpen.value = state.voting.subPhase === 'OPEN';
totalVotes.value = state.voting.totalVotes;
}
@@ -101,6 +103,7 @@ onUnmounted(() => {
<PostcardGrid
ref="gridRef"
:programs="programs"
:awards="awards"
:columns="4"
:rows="2"
/>