背景
之前做了一个随机壁纸接口,但是不知道大家喜欢对壁纸的喜好,所以干脆在实现一个投票功能,让用户给自己喜欢的壁纸进行投票。
原理说明
1.当访问http://demo.com/vote/
时,会从/home/jobs/webs/imgs
及子目录下获取图片列表,然后生成一个投票的vote.html页面,并自动跳转到http://demo.com/vote/vote.html
,点击图片即可选中/取消选中,在右下角始终有个悬浮按钮"投票"
2.当点击"投票"之后,会POST调用http://demo.com/vote/
,把结果记录到home/data/vote_stats.json
,里面记录了得票的图片路径和票数。
3.之后会生成一个result.html页面,并自动跳转到http://demo.com/vote/result.html
,壁纸根据得票数自动排序,最下方有个"返回投票页"的按钮
实战
创建vote目录,存放vote.html和result.html
mkdir /home/jobs/webs/vote
chmod 755 /home/jobs/webs/vote -R
chown nginx:nginx /home/jobs/webs/vote -R
编写lua脚本
cat /etc/nginx/conf.d/vote.lua
package.path = package.path .. ";/usr/local/share/lua/5.1/?.lua;/usr/share/lua/5.1/?.lua"
package.cpath = package.cpath .. ";/usr/local/lib/lua/5.1/?.so;/usr/lib64/lua/5.1/?.so"
local cjson = require "cjson"
local lfs = require "lfs"
-- 获取图片列表
local function get_images(path)
local images = {}
for file in lfs.dir(path) do
if file ~= "." and file ~= ".." then
local full_path = path .. "/" .. file
local attr = lfs.attributes(full_path)
if attr.mode == "file" and (file:match("%.jpg$") or file:match("%.png$")) then
table.insert(images, file)
elseif attr.mode == "directory" then
local sub_images = get_images(full_path)
for _, sub_image in ipairs(sub_images) do
table.insert(images, file .. "/" .. sub_image)
end
end
end
end
return images
end
-- 读取统计结果
local function read_stats()
local stats_path = "/home/data/vote_stats.json"
local stats = {}
local file = io.open(stats_path, "r")
if file then
local content = file:read("*a")
file:close()
stats = cjson.decode(content) or {}
end
return stats
end
-- 保存统计结果
local function save_stats(stats)
local stats_path = "/home/data/vote_stats.json"
local file = io.open(stats_path, "w")
if file then
file:write(cjson.encode(stats))
file:close()
else
ngx.log(ngx.ERR, "Failed to open file: ", stats_path)
end
end
-- 生成投票页面 HTML
local function generate_vote_html()
local images = get_images("/home/jobs/webs/imgs")
-- 生成 HTML 内容
local html = [[
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>壁纸投票</title>
<style>
body { font-family: Arial, sans-serif; }
img { width: 200px; margin: 10px; border: 3px solid transparent; cursor: pointer; }
img.selected { border: 3px solid #007bff; }
.image-container { display: flex; flex-wrap: wrap; }
.image-item { margin: 10px; text-align: center; }
.floating-button {
position: fixed;
bottom: 20px;
right: 20px;
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.floating-button:hover {
background-color: #0056b3;
}
</style>
<script>
function toggleSelection(img) {
img.classList.toggle("selected");
var checkbox = img.parentElement.querySelector('input[type="checkbox"]');
checkbox.checked = !checkbox.checked;
}
</script>
</head>
<body>
<h1>壁纸投票</h1>
<form method="post" action="/vote/">
<div class="image-container">
]]
-- 添加图片和复选框
for _, img in ipairs(images) do
html = html .. string.format([[<div class="image-item">
<input type="checkbox" name="%s" id="%s" style="display: none;">
<label for="%s"><img src="/vote/imgs/%s" alt="%s" onclick="toggleSelection(this)"></label>
</div>]], img, img, img, img, img)
end
html = html .. [[
</div>
<button type="submit" class="floating-button">提交投票</button>
</form>
</body>
</html>
]]
-- 将 HTML 内容写入文件
local html_file_path = "/home/jobs/webs/vote/vote.html"
local file = io.open(html_file_path, "w")
if file then
file:write(html)
file:close()
else
ngx.log(ngx.ERR, "Failed to open file: ", html_file_path)
end
end
-- 生成投票结果页面 HTML
local function generate_result_html()
local stats = read_stats()
-- 按票数排序
local sorted_stats = {}
for img, data in pairs(stats) do
table.insert(sorted_stats, {img = img, count = data.count})
end
table.sort(sorted_stats, function(a, b)
return a.count > b.count
end)
-- 生成 HTML 内容
local html = [[
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>投票结果</title>
<style>
body { font-family: Arial, sans-serif; }
.stats { margin-top: 20px; }
img { width: 200px; margin: 10px; border: 1px solid #ccc; }
.image-container { display: flex; flex-wrap: wrap; }
.image-item { margin: 10px; text-align: center; }
</style>
</head>
<body>
<h1>投票结果</h1>
<div class="image-container">
]]
-- 显示投票结果
for _, data in ipairs(sorted_stats) do
if data.count > 0 then
html = html .. string.format([[<div class="image-item">
<img src="/vote/imgs/%s" alt="%s">
<p>%s: %d 票</p>
</div>]], data.img, data.img, data.img, data.count)
end
end
html = html .. [[
</div>
<a href="/vote/vote.html">返回投票页面</a>
</body>
</html>
]]
-- 将 HTML 内容写入文件
local result_file_path = "/home/jobs/webs/vote/result.html"
local file = io.open(result_file_path, "w")
if file then
file:write(html)
file:close()
else
ngx.log(ngx.ERR, "Failed to open file: ", result_file_path)
end
end
-- 处理投票
local function handle_vote()
-- 确保请求体已读取
ngx.req.read_body()
-- 获取 POST 参数
local args, err = ngx.req.get_post_args()
if not args then
ngx.log(ngx.ERR, "Failed to get POST args: ", err)
ngx.exit(ngx.HTTP_BAD_REQUEST)
end
-- 获取投票人 IP
local voter_ip = ngx.var.remote_addr
-- 读取统计结果
local stats = read_stats()
-- 更新投票数据
for img, _ in pairs(args) do
if not stats[img] then
stats[img] = {count = 0, voters = {}}
end
stats[img].count = stats[img].count + 1
end
-- 保存统计结果
save_stats(stats)
-- 生成 HTML 文件
generate_vote_html() -- 更新投票页面
generate_result_html() -- 生成投票结果页面
-- 重定向到投票结果页面
ngx.redirect("/vote/result.html")
end
-- 处理请求
if ngx.var.request_method == "POST" then
handle_vote()
else
generate_vote_html() -- 生成投票页面
ngx.redirect("/vote/vote.html") -- 重定向到投票页面
end
对应的openresty配置
location /vote/vote.html {
try_files /vote/vote.html =404;
}
location /vote/result.html {
try_files /vote/result.html =404;
}
location /vote/ {
lua_need_request_body on; # 启用请求体读取
content_by_lua_file /etc/nginx/conf.d/vote.lua;
}
# 静态图片服务,用于展示壁纸
location /vote/imgs/ {
alias /home/jobs/webs/imgs/;
}