前言

最近需要开发一个支持100人同时在线投票的应用,考虑到成本和可扩展性,我选择了AWS的Serverless架构。整个过程从需求分析到部署上线,遇到了不少坑,也学到了很多东西。今天分享一下完整的开发和部署过程。

项目需求

  • 支持100人同时在线投票
  • 实时显示投票结果
  • 防止重复投票
  • 支持多个投票房间
  • 成本控制在合理范围内

技术选型

经过对比,我选择了以下技术栈:

后端架构
  • AWS Lambda: 处理投票逻辑,按需付费
  • API Gateway: 提供REST API接口
  • Node.js: 运行时环境
前端架构
  • 原生HTML/CSS/JavaScript: 轻量级,无需复杂框架
  • AWS S3: 静态网站托管
为什么选择Serverless?
  1. 成本优势: 按使用量付费,100人使用预估月成本仅$1-5
  2. 自动扩缩容: 无需担心并发处理
  3. 运维简单: AWS托管,无需维护服务器
  4. 高可用性: 99.9%的服务可用性

开发过程

1. 项目初始化

首先创建项目结构:

mkdir voting-app
cd voting-app
npm init -y
  • 1.
  • 2.
  • 3.
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仓库
aws ecr create-repository \
    --repository-name voting-app \
    --region us-east-1 \
    --image-scanning-configuration scanOnPush=true
  • 1.
  • 2.
  • 3.
  • 4.
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静态网站
# 创建S3存储桶
aws s3 mb s3://voting-app-static-ACCOUNT_ID

# 配置网站托管
aws s3 website s3://voting-app-static-ACCOUNT_ID \
    --index-document index.html

# 上传前端文件
aws s3 cp index.html s3://voting-app-static-ACCOUNT_ID/
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

遇到的问题和解决方案

问题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' };

功能测试

创建投票测试

curl -X POST "https://API_ID.execute-api.us-east-1.amazonaws.com/prod/api/create-vote" \
  -H "Content-Type: application/json" \
  -d '{"title":"测试投票","options":["选项1","选项2"],"duration":300}'
  • 1.
  • 2.
  • 3.

投票功能测试

curl -X POST "https://API_ID.execute-api.us-east-1.amazonaws.com/prod/api/cast-vote" \
  -H "Content-Type: application/json" \
  -d '{"roomId":"ROOM_ID","optionId":"OPTION_ID","voterId":"USER_ID"}'
  • 1.
  • 2.
  • 3.

成本分析

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:

const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();

// 存储投票数据
await dynamodb.put({
    TableName: 'VotingData',
    Item: voteData
}).promise();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

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提升性能:

const redis = require('redis');
const client = redis.createClient({
    host: 'REDIS_ENDPOINT'
});

// 缓存投票数据
await client.setex(`vote:${roomId}`, 3600, JSON.stringify(voteData));
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

监控和运维

CloudWatch监控

  • Lambda函数执行时间和错误率
  • API Gateway请求量和延迟
  • S3访问日志分析

告警设置

aws cloudwatch put-metric-alarm \
    --alarm-name "Lambda-Error-Rate" \
    --alarm-description "Lambda函数错误率告警" \
    --metric-name Errors \
    --namespace AWS/Lambda \
    --statistic Sum \
    --period 300 \
    --threshold 10 \
    --comparison-operator GreaterThanThreshold
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

安全考虑

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架构的优势:

优点

  1. 成本效益: 按需付费,无需预付费用
  2. 自动扩缩容: 无需担心流量峰值
  3. 运维简单: AWS托管,专注业务逻辑
  4. 快速部署: 从开发到上线仅需几小时

不足

  1. 冷启动延迟: Lambda函数首次调用可能较慢
  2. 调试复杂: 本地调试相对困难
  3. 供应商锁定: 与AWS服务深度绑定

适用场景

  • 中小型应用快速原型
  • 流量不稳定的应用
  • 成本敏感的项目
  • 需要快速迭代的产品

下一步计划

  1. 数据持久化: 集成DynamoDB存储历史数据
  2. 实时功能: 使用WebSocket实现真正的实时更新
  3. 用户系统: 添加用户注册和认证功能
  4. 移动端: 开发React Native移动应用
  5. 数据分析: 添加投票结果分析和可视化

部分页面截图

从零开始:借助Amazon Q构建实时投票应用_API

从零开始:借助Amazon Q构建实时投票应用_API_02

从零开始:借助Amazon Q构建实时投票应用_JSON_03

项目地址

完整的项目代码和部署脚本

- 前端代码:  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技术的支持下,代码开发和迭代的效率提高了数倍。