前言
最近需要开发一个支持100人同时在线投票的应用,考虑到成本和可扩展性,我选择了AWS的Serverless架构。整个过程从需求分析到部署上线,遇到了不少坑,也学到了很多东西。今天分享一下完整的开发和部署过程。
项目需求
- 支持100人同时在线投票
- 实时显示投票结果
- 防止重复投票
- 支持多个投票房间
- 成本控制在合理范围内
技术选型
经过对比,我选择了以下技术栈:
后端架构
- AWS Lambda: 处理投票逻辑,按需付费
- API Gateway: 提供REST API接口
- Node.js: 运行时环境
前端架构
- 原生HTML/CSS/JavaScript: 轻量级,无需复杂框架
- AWS S3: 静态网站托管
为什么选择Serverless?
- 成本优势: 按使用量付费,100人使用预估月成本仅$1-5
- 自动扩缩容: 无需担心并发处理
- 运维简单: AWS托管,无需维护服务器
- 高可用性: 99.9%的服务可用性
开发过程
1. 项目初始化
首先创建项目结构:
2. Lambda函数开发
核心的投票逻辑使用Lambda函数实现:
// lambda-deploy.js
const votingData = new Map();
exports.handler = async (event, context) => {
const { httpMethod, path, body } = event;
// CORS headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS'
};
try {
// 创建投票
if (httpMethod === 'POST' && path === '/api/create-vote') {
const { title, options, duration } = JSON.parse(body);
const roomId = generateId();
const voteData = {
id: roomId,
title,
options: options.map(option => ({
id: generateId(),
text: option,
votes: 0,
voters: []
})),
duration: duration || 300,
createdAt: new Date().toISOString(),
isActive: true,
totalVotes: 0
};
votingData.set(roomId, voteData);
return {
statusCode: 200,
headers,
body: JSON.stringify({ roomId, vote: voteData })
};
}
// 投票处理
if (httpMethod === 'POST' && path === '/api/cast-vote') {
const { roomId, optionId, voterId } = JSON.parse(body);
const vote = votingData.get(roomId);
// 防重复投票检查
const hasVoted = vote.options.some(option =>
option.voters.includes(voterId)
);
if (hasVoted) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: '您已经投过票了' })
};
}
// 执行投票
const option = vote.options.find(opt => opt.id === optionId);
if (option) {
option.votes++;
option.voters.push(voterId);
vote.totalVotes++;
return {
statusCode: 200,
headers,
body: JSON.stringify({ success: true, vote: vote })
};
}
}
} catch (error) {
return {
statusCode: 500,
headers,
body: JSON.stringify({ error: error.message })
};
}
};
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
3. 前端界面设计
采用响应式设计,支持移动端访问:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>实时投票应用</title>
<style>
/* 渐变背景和现代化UI设计 */
body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.vote-option {
background: #f7fafc;
border: 2px solid #e2e8f0;
border-radius: 10px;
padding: 15px;
margin: 10px 0;
cursor: pointer;
transition: all 0.3s;
}
.progress-bar {
height: 4px;
background: #e2e8f0;
border-radius: 2px;
margin-top: 8px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #667eea;
transition: width 0.5s ease;
}
</style>
</head>
<body>
<!-- 投票界面HTML结构 -->
</body>
</html>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
AWS部署过程
1. 创建ECR仓库
2. 创建IAM角色
Lambda函数需要执行权限:
aws iam create-role \
--role-name voting-app-lambda-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}'
aws iam attach-role-policy \
--role-name voting-app-lambda-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
3. 部署Lambda函数
# 创建部署包
zip -r voting-app.zip lambda-deploy.js package.json
# 创建Lambda函数
aws lambda create-function \
--function-name voting-app-function \
--runtime nodejs16.x \
--role arn:aws:iam::ACCOUNT_ID:role/voting-app-lambda-role \
--handler lambda-deploy.handler \
--zip-file fileb://voting-app.zip \
--timeout 30 \
--memory-size 256
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
4. 配置API Gateway
# 创建REST API
aws apigateway create-rest-api \
--name voting-app-api \
--description "投票应用API"
# 创建代理资源
aws apigateway create-resource \
--rest-api-id API_ID \
--parent-id ROOT_RESOURCE_ID \
--path-part "{proxy+}"
# 配置ANY方法
aws apigateway put-method \
--rest-api-id API_ID \
--resource-id RESOURCE_ID \
--http-method ANY \
--authorization-type NONE
# 集成Lambda函数
aws apigateway put-integration \
--rest-api-id API_ID \
--resource-id RESOURCE_ID \
--http-method ANY \
--type AWS_PROXY \
--integration-http-method POST \
--uri arn:aws:apigateway:REGION:lambda:path/2015-03-31/functions/LAMBDA_ARN/invocations
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
5. 部署S3静态网站
遇到的问题和解决方案
问题1: S3网站403错误
问题描述: 部署完成后访问S3网站返回403 Forbidden错误
原因分析: S3存储桶默认启用了公共访问阻止设置
解决方案: ```bash
删除公共访问阻止
aws s3api delete-public-access-block \ --bucket voting-app-static-ACCOUNT_ID
设置存储桶策略
aws s3api put-bucket-policy \ --bucket voting-app-static-ACCOUNTID \ --policy '{ "Version": "2012-10-17", "Statement": [{ "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::voting-app-static-ACCOUNTID/*" }] }' ```
问题2: Lambda函数权限问题
问题描述: API Gateway无法调用Lambda函数
解决方案:bash aws lambda add-permission \ --function-name voting-app-function \ --statement-id api-gateway-invoke \ --action lambda:InvokeFunction \ --principal apigateway.amazonaws.com \ --source-arn "arn:aws:execute-api:REGION:ACCOUNT_ID:API_ID/*/*"
问题3: CORS跨域问题
问题描述: 前端无法调用API接口
解决方案: 在Lambda函数中正确设置CORS头部:javascript const headers = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS' };
功能测试
创建投票测试
投票功能测试
成本分析
AWS服务成本预估(月)
服务 | 免费额度 | 超出后价格 | 100人使用预估 |
Lambda | 100万次请求 | $0.20/100万次 | $0.50 |
API Gateway | 100万次调用 | $3.50/100万次 | $2.00 |
S3存储 | 5GB | $0.023/GB | $0.10 |
S3请求 | 2万次GET | $0.40/100万次 | $0.20 |
总计: 约 $2.80/月
相比传统的EC2部署方案(至少$10-20/月),Serverless架构在成本上有明显优势。
性能优化建议
1. 数据持久化
当前版本使用内存存储,生产环境建议集成DynamoDB:
2. 实时更新
集成WebSocket API Gateway实现真正的实时更新:
const apigatewaymanagementapi = new AWS.ApiGatewayManagementApi({
apiVersion: '2018-11-29',
endpoint: 'https://WEBSOCKET_API_ID.execute-api.REGION.amazonaws.com/STAGE'
});
// 推送实时更新
await apigatewaymanagementapi.postToConnection({
ConnectionId: connectionId,
Data: JSON.stringify(updateData)
}).promise();
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
3. 缓存优化
使用ElastiCache Redis提升性能:
监控和运维
CloudWatch监控
- Lambda函数执行时间和错误率
- API Gateway请求量和延迟
- S3访问日志分析
告警设置
安全考虑
1. API限流
// 实现简单的限流逻辑
const rateLimiter = new Map();
function checkRateLimit(ip) {
const now = Date.now();
const windowStart = now - 60000; // 1分钟窗口
if (!rateLimiter.has(ip)) {
rateLimiter.set(ip, []);
}
const requests = rateLimiter.get(ip);
const recentRequests = requests.filter(time => time > windowStart);
if (recentRequests.length >= 100) { // 每分钟最多100次请求
return false;
}
recentRequests.push(now);
rateLimiter.set(ip, recentRequests);
return true;
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
2. 输入验证
function validateInput(data) {
if (!data.title || data.title.length > 200) {
throw new Error('标题长度不能超过200字符');
}
if (!Array.isArray(data.options) || data.options.length < 2) {
throw new Error('至少需要2个选项');
}
data.options.forEach(option => {
if (typeof option !== 'string' || option.length > 100) {
throw new Error('选项长度不能超过100字符');
}
});
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
扩展功能
1. 用户认证
集成AWS Cognito实现用户管理:
const AWS = require('aws-sdk');
const cognito = new AWS.CognitoIdentityServiceProvider();
async function verifyToken(token) {
try {
const params = {
AccessToken: token
};
const user = await cognito.getUser(params).promise();
return user;
} catch (error) {
throw new Error('Token验证失败');
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
2. 投票分析
添加数据分析功能:
function generateAnalytics(vote) {
const totalVotes = vote.totalVotes;
const analytics = {
participationRate: (totalVotes / 100 * 100).toFixed(2) + '%',
mostPopularOption: vote.options.reduce((prev, current) =>
prev.votes > current.votes ? prev : current
),
votingTrend: calculateVotingTrend(vote),
demographics: analyzeDemographics(vote)
};
return analytics;
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
总结
通过这次项目,我深刻体会到了AWS Serverless架构的优势:
优点
- 成本效益: 按需付费,无需预付费用
- 自动扩缩容: 无需担心流量峰值
- 运维简单: AWS托管,专注业务逻辑
- 快速部署: 从开发到上线仅需几小时
不足
- 冷启动延迟: Lambda函数首次调用可能较慢
- 调试复杂: 本地调试相对困难
- 供应商锁定: 与AWS服务深度绑定
适用场景
- 中小型应用快速原型
- 流量不稳定的应用
- 成本敏感的项目
- 需要快速迭代的产品
下一步计划
- 数据持久化: 集成DynamoDB存储历史数据
- 实时功能: 使用WebSocket实现真正的实时更新
- 用户系统: 添加用户注册和认证功能
- 移动端: 开发React Native移动应用
- 数据分析: 添加投票结果分析和可视化
部分页面截图
项目地址
完整的项目代码和部署脚本
- 前端代码: voting-app-frontend
index-lambda.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>实时投票应用 - AWS版</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
overflow: hidden;
}
.header {
background: #4a5568;
color: white;
padding: 20px;
text-align: center;
}
.content {
padding: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #2d3748;
}
input, select, button {
width: 100%;
padding: 12px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
}
button {
background: #667eea;
color: white;
border: none;
cursor: pointer;
font-weight: bold;
transition: background 0.3s;
}
button:hover {
background: #5a67d8;
}
.vote-option {
background: #f7fafc;
border: 2px solid #e2e8f0;
border-radius: 10px;
padding: 15px;
margin: 10px 0;
cursor: pointer;
transition: all 0.3s;
position: relative;
overflow: hidden;
}
.vote-option:hover {
border-color: #667eea;
transform: translateY(-2px);
}
.vote-option.voted {
border-color: #48bb78;
background: #f0fff4;
}
.option-text {
font-weight: bold;
margin-bottom: 5px;
}
.vote-count {
color: #666;
font-size: 14px;
}
.progress-bar {
height: 4px;
background: #e2e8f0;
border-radius: 2px;
margin-top: 8px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #667eea;
transition: width 0.5s ease;
}
.status {
text-align: center;
padding: 15px;
margin: 20px 0;
border-radius: 8px;
}
.status.active {
background: #f0fff4;
color: #22543d;
border: 1px solid #9ae6b4;
}
.status.ended {
background: #fed7d7;
color: #742a2a;
border: 1px solid #fc8181;
}
.hidden {
display: none;
}
.error {
background: #fed7d7;
color: #742a2a;
padding: 10px;
border-radius: 8px;
margin: 10px 0;
}
.success {
background: #f0fff4;
color: #22543d;
padding: 10px;
border-radius: 8px;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗳️ 实时投票应用</h1>
<p>AWS Serverless版本</p>
</div>
<div class="content">
<!-- 创建投票界面 -->
<div id="createVoteSection">
<h2>创建新投票</h2>
<form id="createVoteForm">
<div class="form-group">
<label for="voteTitle">投票标题:</label>
<input type="text" id="voteTitle" required placeholder="请输入投票标题">
</div>
<div class="form-group">
<label>投票选项:</label>
<input type="text" id="option1" required placeholder="选项 1">
<input type="text" id="option2" required placeholder="选项 2" style="margin-top: 10px;">
<input type="text" id="option3" placeholder="选项 3 (可选)" style="margin-top: 10px;">
<input type="text" id="option4" placeholder="选项 4 (可选)" style="margin-top: 10px;">
</div>
<div class="form-group">
<label for="duration">投票时长 (秒):</label>
<select id="duration">
<option value="60">1分钟</option>
<option value="300" selected>5分钟</option>
<option value="600">10分钟</option>
<option value="1800">30分钟</option>
</select>
</div>
<button type="submit">创建投票</button>
</form>
<hr style="margin: 30px 0;">
<h2>加入现有投票</h2>
<div class="form-group">
<label for="roomId">投票房间ID:</label>
<input type="text" id="roomId" placeholder="请输入房间ID">
</div>
<div class="form-group">
<label for="userName">您的姓名:</label>
<input type="text" id="userName" placeholder="请输入您的姓名">
</div>
<button onclick="joinVote()">加入投票</button>
</div>
<!-- 投票界面 -->
<div id="votingSection" class="hidden">
<div id="voteStatus" class="status"></div>
<h2 id="voteTitle"></h2>
<div id="voteOptions"></div>
<div id="voteResults" class="hidden">
<h3>投票结果</h3>
<div id="resultsChart"></div>
</div>
<button onclick="backToHome()" style="margin-top: 20px; background: #718096;">返回首页</button>
<button onclick="refreshVote()" style="margin-top: 20px; background: #38a169;">刷新结果</button>
</div>
<div id="errorMessage" class="error hidden"></div>
<div id="successMessage" class="success hidden"></div>
</div>
</div>
<script>
// API Gateway端点
const API_BASE_URL = 'https://sb2yi89g0g.execute-api.us-east-1.amazonaws.com/prod';
let currentVote = null;
let hasVoted = false;
let voterId = generateId();
// 创建投票
document.getElementById('createVoteForm').addEventListener('submit', async (e) => {
e.preventDefault();
const title = document.getElementById('voteTitle').value;
const options = [
document.getElementById('option1').value,
document.getElementById('option2').value,
document.getElementById('option3').value,
document.getElementById('option4').value
].filter(option => option.trim() !== '');
const duration = parseInt(document.getElementById('duration').value);
try {
const response = await fetch(`${API_BASE_URL}/api/create-vote`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, options, duration })
});
const data = await response.json();
if (response.ok) {
showSuccess(`投票创建成功!房间ID: ${data.roomId}`);
document.getElementById('roomId').value = data.roomId;
} else {
showError(data.error || '创建投票失败');
}
} catch (error) {
showError('创建投票失败: ' + error.message);
}
});
// 加入投票
async function joinVote() {
const roomId = document.getElementById('roomId').value.trim();
const userName = document.getElementById('userName').value.trim();
if (!roomId || !userName) {
showError('请输入房间ID和姓名');
return;
}
try {
const response = await fetch(`${API_BASE_URL}/api/vote/${roomId}`);
const vote = await response.json();
if (response.ok) {
currentVote = vote;
showVotingInterface(vote);
} else {
showError(vote.error || '加入投票失败');
}
} catch (error) {
showError('加入投票失败: ' + error.message);
}
}
// 显示投票界面
function showVotingInterface(vote) {
document.getElementById('createVoteSection').classList.add('hidden');
document.getElementById('votingSection').classList.remove('hidden');
document.getElementById('voteTitle').textContent = vote.title;
const statusDiv = document.getElementById('voteStatus');
if (vote.isActive) {
statusDiv.textContent = '投票进行中...';
statusDiv.className = 'status active';
} else {
statusDiv.textContent = '投票已结束';
statusDiv.className = 'status ended';
}
displayVoteOptions(vote);
}
// 显示投票选项
function displayVoteOptions(vote) {
const optionsDiv = document.getElementById('voteOptions');
optionsDiv.innerHTML = '';
vote.options.forEach(option => {
const optionDiv = document.createElement('div');
optionDiv.className = 'vote-option';
optionDiv.onclick = () => castVote(option.id);
const percentage = vote.totalVotes > 0 ? (option.votes / vote.totalVotes * 100).toFixed(1) : 0;
optionDiv.innerHTML = `
<div class="option-text">${option.text}</div>
<div class="vote-count">${option.votes} 票 (${percentage}%)</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${percentage}%"></div>
</div>
`;
optionsDiv.appendChild(optionDiv);
});
}
// 投票
async function castVote(optionId) {
if (!currentVote.isActive) {
showError('投票已结束');
return;
}
if (hasVoted) {
showError('您已经投过票了');
return;
}
try {
const response = await fetch(`${API_BASE_URL}/api/cast-vote`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
roomId: currentVote.id,
optionId: optionId,
voterId: voterId
})
});
const data = await response.json();
if (response.ok) {
hasVoted = true;
currentVote = data.vote;
displayVoteOptions(currentVote);
showSuccess('投票成功!');
} else {
showError(data.error || '投票失败');
}
} catch (error) {
showError('投票失败: ' + error.message);
}
}
// 刷新投票结果
async function refreshVote() {
if (!currentVote) return;
try {
const response = await fetch(`${API_BASE_URL}/api/vote/${currentVote.id}`);
const vote = await response.json();
if (response.ok) {
currentVote = vote;
displayVoteOptions(vote);
showSuccess('结果已刷新');
} else {
showError('刷新失败');
}
} catch (error) {
showError('刷新失败: ' + error.message);
}
}
// 显示错误信息
function showError(message) {
const errorDiv = document.getElementById('errorMessage');
errorDiv.textContent = message;
errorDiv.classList.remove('hidden');
setTimeout(() => {
errorDiv.classList.add('hidden');
}, 5000);
}
// 显示成功信息
function showSuccess(message) {
const successDiv = document.getElementById('successMessage');
successDiv.textContent = message;
successDiv.classList.remove('hidden');
setTimeout(() => {
successDiv.classList.add('hidden');
}, 3000);
}
// 返回首页
function backToHome() {
document.getElementById('votingSection').classList.add('hidden');
document.getElementById('createVoteSection').classList.remove('hidden');
currentVote = null;
hasVoted = false;
}
// 生成随机ID
function generateId() {
return Math.random().toString(36).substr(2, 9);
}
</script>
</body>
</html>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.
- 254.
- 255.
- 256.
- 257.
- 258.
- 259.
- 260.
- 261.
- 262.
- 263.
- 264.
- 265.
- 266.
- 267.
- 268.
- 269.
- 270.
- 271.
- 272.
- 273.
- 274.
- 275.
- 276.
- 277.
- 278.
- 279.
- 280.
- 281.
- 282.
- 283.
- 284.
- 285.
- 286.
- 287.
- 288.
- 289.
- 290.
- 291.
- 292.
- 293.
- 294.
- 295.
- 296.
- 297.
- 298.
- 299.
- 300.
- 301.
- 302.
- 303.
- 304.
- 305.
- 306.
- 307.
- 308.
- 309.
- 310.
- 311.
- 312.
- 313.
- 314.
- 315.
- 316.
- 317.
- 318.
- 319.
- 320.
- 321.
- 322.
- 323.
- 324.
- 325.
- 326.
- 327.
- 328.
- 329.
- 330.
- 331.
- 332.
- 333.
- 334.
- 335.
- 336.
- 337.
- 338.
- 339.
- 340.
- 341.
- 342.
- 343.
- 344.
- 345.
- 346.
- 347.
- 348.
- 349.
- 350.
- 351.
- 352.
- 353.
- 354.
- 355.
- 356.
- 357.
- 358.
- 359.
- 360.
- 361.
- 362.
- 363.
- 364.
- 365.
- 366.
- 367.
- 368.
- 369.
- 370.
- 371.
- 372.
- 373.
- 374.
- 375.
- 376.
- 377.
- 378.
- 379.
- 380.
- 381.
- 382.
- 383.
- 384.
- 385.
- 386.
- 387.
- 388.
- 389.
- 390.
- 391.
- 392.
- 393.
- 394.
- 395.
- 396.
- 397.
- 398.
- 399.
- 400.
- 401.
- 402.
- 403.
- 404.
- 405.
- 406.
- 407.
- 408.
- 409.
- 410.
- 411.
- 412.
- 413.
- 414.
- 415.
- 416.
- 417.
- 418.
- 419.
- 420.
- 421.
- 422.
- 423.
- 424.
- 425.
- 426.
- 427.
- 428.
- 429.
- 430.
- 431.
- 432.
- 433.
- 434.
- 435.
- 436.
- 437.
- 438.
- 439.
- 440.
- 441.
- 442.
- 443.
- 444.
- 445.
- 后端代码: voting-app-backend
lambda-deploy.js
// AWS Lambda版本的投票应用
const AWS = require('aws-sdk');
// 简化版投票处理器
const votingData = new Map();
exports.handler = async (event, context) => {
const { httpMethod, path, body, queryStringParameters } = event;
// CORS headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS'
};
// 处理OPTIONS请求
if (httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: ''
};
}
try {
// 创建投票
if (httpMethod === 'POST' && path === '/api/create-vote') {
const { title, options, duration } = JSON.parse(body);
const roomId = generateId();
const voteData = {
id: roomId,
title,
options: options.map(option => ({
id: generateId(),
text: option,
votes: 0,
voters: []
})),
duration: duration || 300,
createdAt: new Date().toISOString(),
isActive: true,
totalVotes: 0
};
votingData.set(roomId, voteData);
return {
statusCode: 200,
headers,
body: JSON.stringify({ roomId, vote: voteData })
};
}
// 获取投票信息
if (httpMethod === 'GET' && path.startsWith('/api/vote/')) {
const roomId = path.split('/').pop();
const vote = votingData.get(roomId);
if (!vote) {
return {
statusCode: 404,
headers,
body: JSON.stringify({ error: '投票不存在' })
};
}
return {
statusCode: 200,
headers,
body: JSON.stringify(vote)
};
}
// 投票
if (httpMethod === 'POST' && path === '/api/cast-vote') {
const { roomId, optionId, voterId } = JSON.parse(body);
const vote = votingData.get(roomId);
if (!vote || !vote.isActive) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: '投票已结束或不存在' })
};
}
// 检查是否已投票
const hasVoted = vote.options.some(option =>
option.voters.includes(voterId)
);
if (hasVoted) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: '您已经投过票了' })
};
}
// 投票
const option = vote.options.find(opt => opt.id === optionId);
if (option) {
option.votes++;
option.voters.push(voterId);
vote.totalVotes++;
return {
statusCode: 200,
headers,
body: JSON.stringify({
success: true,
vote: vote
})
};
}
}
return {
statusCode: 404,
headers,
body: JSON.stringify({ error: 'Not Found' })
};
} catch (error) {
return {
statusCode: 500,
headers,
body: JSON.stringify({ error: error.message })
};
}
};
function generateId() {
return Math.random().toString(36).substr(2, 9);
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
package.json
{
"name": "voting-app",
"version": "1.0.0",
"description": "实时投票应用",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"socket.io": "^4.7.2",
"redis": "^4.6.7",
"cors": "^2.8.5",
"uuid": "^9.0.0"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 部署脚本: voting-app-deploy on aws
#!/bin/bash
# AWS投票应用部署脚本
set -e
echo "🚀 开始部署投票应用到AWS..."
# 配置变量
APP_NAME="voting-app"
REGION="us-east-1"
ECR_REPO_NAME="voting-app"
# 1. 创建ECR仓库
echo "📦 创建ECR仓库..."
aws ecr create-repository \
--repository-name $ECR_REPO_NAME \
--region $REGION \
--image-scanning-configuration scanOnPush=true \
|| echo "ECR仓库可能已存在"
# 获取ECR登录令牌
echo "🔐 登录ECR..."
aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $(aws sts get-caller-identity --query Account --output text).dkr.ecr.$REGION.amazonaws.com
# 2. 构建Docker镜像
echo "🔨 构建Docker镜像..."
docker build -t $APP_NAME .
# 标记镜像
ECR_URI=$(aws sts get-caller-identity --query Account --output text).dkr.ecr.$REGION.amazonaws.com/$ECR_REPO_NAME:latest
docker tag $APP_NAME:latest $ECR_URI
# 3. 推送镜像到ECR
echo "📤 推送镜像到ECR..."
docker push $ECR_URI
# 4. 创建App Runner服务配置
cat > apprunner.yaml << EOF
version: 1.0
runtime: docker
build:
commands:
build:
- echo "Using pre-built Docker image"
run:
runtime-version: latest
command: npm start
network:
port: 3000
env: PORT
env:
- name: NODE_ENV
value: production
EOF
echo "✅ 部署准备完成!"
echo ""
echo "📋 下一步操作:"
echo "1. 在AWS控制台创建App Runner服务"
echo "2. 使用ECR镜像: $ECR_URI"
echo "3. 配置环境变量和网络设置"
echo ""
echo "🌐 或者使用ECS部署:"
echo "aws ecs create-cluster --cluster-name voting-app-cluster"
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
结语
Serverless架构为现代应用开发提供了新的可能性。虽然有一定的学习曲线,但其带来的成本优势和运维简化是显而易见的。希望这篇文章能够帮助到正在考虑使用Serverless架构的开发者们。
借助Amazon Q可以实现快速APP的开发和部署,在AI技术的支持下,代码开发和迭代的效率提高了数倍。