用openresty和lua实现壁纸投票功能

发布于:2025-02-21 ⋅ 阅读:(15) ⋅ 点赞:(0)

背景

之前做了一个随机壁纸接口,但是不知道大家喜欢对壁纸的喜好,所以干脆在实现一个投票功能,让用户给自己喜欢的壁纸进行投票。

原理说明

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/;
}

效果

在这里插入图片描述

在这里插入图片描述