Browse Source

首次提交

a158 8 tháng trước cách đây
mục cha
commit
53c171d42d

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+################################################################################
+# 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。
+################################################################################
+
+/.vs

+ 2 - 0
build.sh

@@ -0,0 +1,2 @@
+#!/bin/bash
+echo 'INSTALL SUCCESS'

+ 36 - 0
target/aliyun/ai.lua

@@ -0,0 +1,36 @@
+local http = require("socket.http")
+local ltn12 = require("ltn12")
+local cjson = require("cjson")
+
+local M = {}
+local url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"
+
+M.ask = function(key,content,model)
+    -- 构造 JSON 格式的请求数据
+    local payload = {
+        model = model,
+        messages = content
+    }
+    local response_body = {}
+
+    local res, code, response_headers, status = http.request{
+        url = url,
+        method = "POST",
+        headers = {
+            ["Authorization"] = "Bearer " .. key,
+            ["Content-Type"] = "application/json",
+        },
+        source = ltn12.source.string(cjson.encode(payload)),
+        sink = ltn12.sink.table(response_body)
+    }
+    if code == 200 then
+        local data = cjson.decode(table.concat(response_body))
+        return true,{content = data.choices[1].message.content,reasoning_content = data.choices[1].reasoning_content}
+    else
+        return false, "请求失败,错误描述:" .. table.concat(response_body)
+    end
+end
+
+return M
+
+

+ 191 - 0
target/aliyun/email.lua

@@ -0,0 +1,191 @@
+local M = {}
+
+require("app.app")
+local http = require("fwutils.httpclient")
+
+
+
+M.get_token = function(app_id,app_secret)
+    local token = cache.get_json("aliyun_email_token")
+    if token then
+        if token.expires > os.time() + 60*60 then
+            return true,token.access_token
+        end
+    end
+
+    local url = "https://alimail-cn.aliyuncs.com/oauth2/v2.0/token"
+    local body = "grant_type=client_credentials&client_id="..app_id.."&client_secret="..app_secret
+    local result,err = http.post(url,{
+    ["Content-Type"] = "application/x-www-form-urlencoded"
+    },body)
+    if not result then
+        return false,err
+    end 
+    local data = cjson.decode(err)
+    if data.error ~= nil and data.error ~= "" then
+        return false,data.error_description
+    end
+    
+    cache.set_json("aliyun_email_token",{
+        access_token = data.access_token,
+        expires = os.time() + data.expires_in
+    })
+    return true,data.access_token
+end
+
+M.create_msg = function(from,from_name,to,to_name,title,content,app_id,app_secret)
+    local ok,access_token = M.get_token(app_id,app_secret)
+    if not ok then
+        return false,access_token
+    end
+    local body = {
+        message = {
+            subject = title,
+            summary = content, 
+            priority = "PRY_HIGH",
+            isReadReceiptRequested = false,
+            from = {
+                email = from,
+                name = from_name
+            },
+            toRecipients = {
+                {
+                    email = to,
+                    name = to_name
+                }
+            },
+            body = {
+                bodyText = content,
+                bodyHtml = content
+            }
+        }
+    }
+    local url = "https://alimail-cn.aliyuncs.com/v2/users/"..from.."/messages"
+    local result,err = http.post(url,{
+        ["Content-Type"] = "application/json",
+        ["Authorization"] = "Bearer "..access_token
+    },cjson.encode(body))
+    if not result then
+        return false,err
+    end
+    local data = cjson.decode(err)
+    if data == nil or data.message == nil or data.message.id == nil then
+        return false,err
+    end
+    return true,data.message.id
+end
+M.send = function(from,from_name,to,to_name,title,content,app_id,app_secret)
+    local ok,access_token = M.get_token(app_id,app_secret)
+    if not ok then
+        return false,access_token
+    end
+    local ok,msg_id = M.create_msg(from,from_name,to,to_name,title,content,app_id,app_secret)
+    if not ok then
+        return false,msg_id
+    end
+    local body = {
+        saveToSentItems = true
+    }
+    local url = "https://alimail-cn.aliyuncs.com/v2/users/"..from.."/messages/"..msg_id.."/send"
+    local result,err = http.post(url,{ 
+        ["Content-Type"] = "application/json",
+        ["Authorization"] = "Bearer "..access_token
+    },cjson.encode(body))
+    if not result then
+        return false,"send error:"..err
+    end
+    if err == "{}" then
+        return true
+    end
+    return false,"send error:"..err
+    --return true
+end
+M.create_user = function(email_name,password,nickname,jobtitle,app_id,app_secret)
+    local ok,access_token = M.get_token(app_id,app_secret)
+    if not ok then
+        return false,access_token
+    end
+    local body = {
+        email = email_name,
+        password = password,
+        name = nickname,
+        jobTitle = jobtitle,
+        departmentIds = {config.email.department_id},
+    }
+    local url = "https://alimail-cn.aliyuncs.com/v2/users"
+    local result,err = http.post(url,{ 
+        ["Content-Type"] = "application/json",
+        ["Authorization"] = "Bearer "..access_token
+    },cjson.encode(body))
+    if not result then
+        return false,"send error:"..err
+    end
+
+    local data = cjson.decode(err)
+    if data.email == email_name then
+        return true
+    end
+    return false,"create user error:"..err 
+end
+M.delete_user = function(email_name,app_id,app_secret)
+    local ok,access_token = M.get_token(app_id,app_secret)
+    if not ok then
+        return false,access_token
+    end
+    local url = "https://alimail-cn.aliyuncs.com/v2/users/"..email_name
+    local result,err = http.delete(url,{ 
+        ["Content-Type"] = "application/json",
+        ["Authorization"] = "Bearer "..access_token
+    })
+    if not result then
+        return false,"send error:"..err
+    end
+    return true
+end
+M.getDepartment = function(id,app_id,app_secret)
+    local ok,access_token = M.get_token(app_id,app_secret)
+    if not ok then
+        return false,access_token
+    end
+
+
+    local url = "https://alimail-cn.aliyuncs.com/v2/departments/"..id
+    local result,err = http.get(url,{ 
+        ["Content-Type"] = "application/json",
+        ["Authorization"] = "Bearer "..access_token
+    })
+
+    if not result then
+        return false,"send error:"..err
+    end
+    print("getDepartment")
+    print("result",result)
+    print("err",type(err)) 
+    print("err[data]",err)
+
+    return true
+end
+M.getDepartmentList = function(app_id,app_secret)
+    local ok,access_token = M.get_token(app_id,app_secret)
+    if not ok then
+        return false,access_token
+    end
+   
+    local url = "https://alimail-cn.aliyuncs.com/v2/departments/$root/chain"
+    local result,err = http.get(url,{ 
+        ["Content-Type"] = "application/json",
+        ["Authorization"] = "Bearer "..access_token
+    })
+    if not result then
+        return false,"send error:"..err
+    end
+    print("getDepartmentList")
+    print("result",result)
+    print("err",type(err)) 
+    print("err[data]",err)
+
+    return true
+end
+
+return M
+

+ 52 - 0
target/base64.lua

@@ -0,0 +1,52 @@
+local M = {}
+
+-- Base64 字符集
+local b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
+-- 创建 Base64 反向查找表
+local b64lookup = {}
+for i = 1, #b64chars do
+    b64lookup[string.sub(b64chars, i, i)] = i - 1
+end
+
+M.encode = function(input)
+    local output = {}
+    local len = #input
+    for i = 1, len, 3 do
+        local a, b, c = string.byte(input, i, i + 2)
+        local chunk = (a or 0) << 16 | (b or 0) << 8 | (c or 0)
+
+        output[#output + 1] = b64chars:sub(((chunk >> 18) & 0x3F) + 1, ((chunk >> 18) & 0x3F) + 1)
+        output[#output + 1] = b64chars:sub(((chunk >> 12) & 0x3F) + 1, ((chunk >> 12) & 0x3F) + 1)
+        output[#output + 1] = b and b64chars:sub(((chunk >> 6) & 0x3F) + 1, ((chunk >> 6) & 0x3F) + 1) or "="
+        output[#output + 1] = c and b64chars:sub((chunk & 0x3F) + 1, (chunk & 0x3F) + 1) or "="
+    end
+    return table.concat(output)
+end
+
+-- Base64 解码函数
+M.decode = function(input)
+    input = input:gsub("%s", ""):gsub("=", "")  -- 去除空白和填充符
+    local output = {}
+    for i = 1, #input, 4 do
+        local a, b, c, d = b64lookup[input:sub(i, i)], b64lookup[input:sub(i + 1, i + 1)],
+                            b64lookup[input:sub(i + 2, i + 2)] or 0, b64lookup[input:sub(i + 3, i + 3)] or 0
+        local chunk = (a << 18) | (b << 12) | (c << 6) | d
+
+        output[#output + 1] = string.char((chunk >> 16) & 0xFF)
+        if input:sub(i + 2, i + 2) ~= "" then
+            output[#output + 1] = string.char((chunk >> 8) & 0xFF)
+        end
+        if input:sub(i + 3, i + 3) ~= "" then
+            output[#output + 1] = string.char(chunk & 0xFF)
+        end
+    end
+    return table.concat(output)
+end
+
+
+
+
+
+return M
+
+

+ 40 - 0
target/cache.lua

@@ -0,0 +1,40 @@
+local M = {}
+
+local localstorage = require("localstorage")
+local fw = require("fastweb")
+local cjson = require("cjson")
+
+M.start = function(dirpath)
+    local storage = localstorage.new()
+    fw.set_ptr("localstorage_cache",storage:self())
+    if storage:open(dirpath) == false then
+        return false,storage:last_error()
+    end
+    return true
+end
+
+M.close = function()
+    localstorage.new(localstorage_cache):close()
+end
+M.get = function(key)
+    return localstorage.new(localstorage_cache):read(key)
+end
+M.get_json = function(key)
+    local value = M.get(key)
+    if value then
+        return cjson.decode(value)
+    end
+    return nil
+end
+M.set = function(key,value)
+    localstorage.new(localstorage_cache):write(key,value)
+end
+M.set_json = function(key,value)
+    M.set(key,cjson.encode(value))
+end
+M.del = function(key)
+    localstorage.new(localstorage_cache):del(key)
+end
+return M
+
+

+ 117 - 0
target/fwutils/acl.lua

@@ -0,0 +1,117 @@
+local utils = require("utils")
+local fw = require("fastweb")
+local config = require("fwutils.config")
+local M = {}
+
+
+-- 更新
+M.update = function(role_id,conn)
+    -- 查询权限表
+    local select = conn:select()
+    select:table("fw_role_permissions")
+    select:where_expression("AND delete_time IS NULL")
+    if role_id ~= nil then
+        select:where_i32("role_id", "=", role_id)
+    end
+    local result = select:query()
+
+
+    local bc = {}
+
+    while result:next() do
+        local id = result:get("id")
+        local path = result:get("path")
+        local role_id = tostring(result:get("role_id"))
+        local action = result:get("action")
+        local desc = result:get("desc")
+        local create_time = result:get("create_time")
+        local update_time = result:get("update_time")
+        local delete_time = result:get("delete_time")
+        -- local public = result:get("public")
+
+        if bc[role_id] == nil then
+            bc[role_id] = {}
+        end
+        if bc[role_id]["public"] == nil then
+            bc[role_id]["public"] = {}
+        end
+        if bc[role_id]["private"] == nil then
+            bc[role_id]["private"] = {}
+        end
+        -- 处理 action 字段,将其切分为表或空表
+        local actions_tbl = {}
+        if action and action ~= "" then
+            for act in string.gmatch(action, "([^,]+)") do
+                table.insert(actions_tbl, act)
+            end
+        end
+
+        local item = {
+            create_time = create_time,
+            update_time = update_time,
+            delete_time = delete_time,
+            action = actions_tbl,
+            desc = desc,
+        }
+        -- if public == 1 then
+        --     bc[role_id]["public"][path] = item
+        -- else
+        --     bc[role_id]["private"][path] = item
+        -- end
+        bc[role_id][path] = item
+    end
+    local code = "return " .. require("serpent").serialize(bc, {comment = false})
+    utils.save_file(fw.website_dir().."/"..(config.path.luabytecode:gsub("%.", "/")).."/acl_bc.lua",code)
+    return true
+end
+-- 匹配
+M.match = function(cfg)
+
+    local function match_path(path, patterns)
+
+        -- print("[match_path] path:",path)
+        for pattern, v in pairs(patterns) do
+           
+            -- 如果是正则(以^开头),用string.match,否则精确匹配
+            if string.sub(pattern, 1, 1) == "^" then
+                
+                if string.match(path, pattern) then
+                    -- print("[TRUE] pattern:",pattern,",path:",path)
+                    return true, v 
+                -- else
+                    -- print("[FALSE] pattern:",pattern,",path:",path)
+                end
+            else
+                if path == pattern then
+                    return true, v
+                end
+            end
+        end
+        return false, nil
+    end
+    -- 检查action
+    local function check_action(actions,action)
+        if actions == nil or #actions == 0 then 
+            return true
+        end
+    
+        for _,v in pairs(actions) do
+            if v == action then
+                return true
+            end
+        end
+        return false, "action not match"
+    end
+    local role_id_str = string.format("%d",cfg.role_id())
+    local acl_bc = require(config.path.luabytecode..".acl_bc")
+    if acl_bc[role_id_str] == nil then
+        return false,"role id("..role_id_str..") acl not found"
+    end
+    local result, item = match_path(cfg.filepath(), acl_bc[role_id_str])
+    if result then
+        return check_action(item.action,cfg.action())
+    end 
+    return false,"path("..cfg.filepath()..") acl not found"
+end
+
+return M

+ 22 - 0
target/fwutils/config.lua

@@ -0,0 +1,22 @@
+return {
+    -- 服务器ID,如果REDIS或MYSQL与其它服务公用必须更改此参数,要求唯一
+    server_id = "TEACHER_1234567890",
+    
+    path = {
+        luabytecode = "cache.fw.luabytecode"
+    },
+    token = {
+        -- 超时时间
+        expire = 3600 * 24 * 30,
+        -- 加密算法
+        algorithm = "aes-256",
+        -- 加密模式
+        mode = "cbc",
+        -- 加密密钥
+        key = "kangDzFLc3MweDQH",
+    },
+    db = {
+        redis_pool_name = "rdb",
+        mysql_pool_name = "db",
+    }
+}

+ 41 - 0
target/fwutils/init.lua

@@ -0,0 +1,41 @@
+local mysql_pool = require("mysql.pool")
+local fw = require("fastweb")
+local utils = require("utils")
+
+
+local M = {}
+
+
+M.initialization = function(conn)
+    
+    local result,err = M.__get_guest_role_id(conn)
+    if result == false then
+        return false,err
+    end
+
+    return true
+end
+M.__get_guest_role_id = function(mysql_conn)
+    local ppst = mysql_conn:setsql("SELECT * FROM fw_role WHERE guest = 1")
+    local result = ppst:query()
+    if result:row_count() ~= 1 then
+        return false,"role guest not found"
+    end
+
+    result:next()
+    local bc = {
+        GUEST_ROLE_ID = result:get("id"),
+    }
+    local config = require("fwutils.config")
+    local code = "return " .. require("serpent").serialize(bc, {comment = false})
+    utils.save_file(fw.website_dir().."/"..(config.path.luabytecode:gsub("%.", "/")).."/fwcache.lua",code)
+    return true
+end
+M.guest_role_id = function()
+    local config = require("fwutils.config")
+    return require(config.path.luabytecode..".fwcache").GUEST_ROLE_ID
+end
+
+
+
+return M

+ 104 - 0
target/fwutils/menu.lua

@@ -0,0 +1,104 @@
+local fw = require("fastweb")
+local config = require("fwutils.config")
+local M = {}
+-- CREATE TABLE `fw_menu` (
+--   `id` int NOT NULL AUTO_INCREMENT,
+--   `role_id` int DEFAULT NULL,
+--   `title` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+--   `path` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+--   `icon` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+--   `sort` int DEFAULT NULL,
+--   `parent_id` int DEFAULT NULL,
+--   PRIMARY KEY (`id`)
+-- ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- 更新
+M.update = function(role_id,conn)
+    -- 查询菜单表
+    local select = conn:select()
+    select:table("fw_menu")
+    select:where_expression("AND delete_time IS NULL")
+    if role_id ~= nil then
+        select:where_i32("role_id", "=", role_id)
+    end
+    local result = select:query()
+
+    local bc = {}
+    local items_by_id = {}  -- 通过id快速查找菜单项:{id = {role_id, title, item, parent_id}}
+    local items_with_parent = {}  -- 存储有父级的菜单项
+
+    -- 第一遍:读取所有菜单项并存储
+    while result:next() do
+        local id = result:get("id")
+        local role_id = tostring(result:get("role_id"))
+        local title = result:get("title")
+        local path = result:get("path")
+        local icon = result:get("icon")
+        local sort = result:get("sort")
+        local parent_id = result:get("parent_id")
+
+        if not title or title == "" then
+            goto continue
+        end
+
+        if bc[role_id] == nil then
+            bc[role_id] = {}
+        end
+
+        local item = {
+            path = path,
+            icon = icon,
+            sort = sort,
+        }
+
+        -- 存储所有菜单项信息
+        items_by_id[id] = {
+            role_id = role_id,
+            title = title,
+            item = item,
+            parent_id = parent_id
+        }
+
+        -- 如果parent_id为空,则作为顶层菜单项
+        if parent_id == nil or parent_id == 0 then
+            bc[role_id][title] = item
+        else
+            -- 有父级,记录下来稍后处理
+            table.insert(items_with_parent, {
+                id = id,
+                role_id = role_id,
+                title = title,
+                item = item,
+                parent_id = parent_id
+            })
+        end
+
+        ::continue::
+    end
+
+    -- 第二遍:处理有父级的菜单项,构建children结构
+    for _, menu_item in ipairs(items_with_parent) do
+        local parent_info = items_by_id[menu_item.parent_id]
+        if parent_info then
+            local parent_item = parent_info.item
+
+            -- 如果父项还没有children表,创建它
+            if not parent_item.children then
+                parent_item.children = {}
+            end
+
+            -- 将子项添加到父项的children中
+            parent_item.children[menu_item.title] = menu_item.item
+        end
+    end
+
+    local code = "return " .. require("serpent").serialize(bc, {comment = false})
+    utils.save_file(fw.website_dir().."/"..(config.path.luabytecode:gsub("%.", "/")).."/menu_bc.lua",code)
+    return true
+end
+M.get = function(role_id)
+    local menu_bc = require(config.path.luabytecode..".menu_bc")
+    return menu_bc[string.format("%d",role_id)]
+end
+
+return M

+ 64 - 0
target/fwutils/request_config.lua

@@ -0,0 +1,64 @@
+local request = require("fastweb.request")
+local token_module = require("fwutils.token")
+local config = require("fwutils.config")
+local cjson = require("cjson")
+local utils = require("utils")
+local fwutils_init = require("fwutils.init")
+local M = {}
+M.__filepath = nil 
+M.__method = nil
+M.__url_param = nil
+M.__action = nil
+M.__user_data = nil
+M.__role_id = nil
+M.__ext = nil
+
+M.init = function(website_config)
+    M.__filepath = request.filepath()
+    M.__method = request.method()
+    M.__url_param = request.url_param()
+    M.__action = nil
+    M.__user_data = nil
+    M.__role_id = nil
+    M.__ext = utils.ext(M.__filepath)
+    -- 修饰路径
+    if M.__filepath == "/" then
+        M.__filepath = website_config.default_filepath
+    end
+    -- 获取动作
+    if M.__url_param ~= nil then
+        M.__action = M.__url_param["action"]
+    end
+    -- 获取TOKEN
+    local token = string.match(request.header("Cookie"),"token=(%w+)")
+    if token ~= nil and token ~= "" then
+        local result,user_data = token_module.get(token)
+        -- print("TOKEN_DATA:",cjson.encode(user_data)) 
+        if result and user_data ~= nil and user_data["role_id"] ~= nil and user_data["role_id"] > 0 then
+            M.__user_data = user_data
+            M.__role_id = user_data["role_id"]
+            request.set("user_data",cjson.encode(user_data))
+        else
+            M.__role_id = fwutils_init.guest_role_id()
+        end
+    else
+        -- 访客
+        M.__role_id = fwutils_init.guest_role_id()
+    end
+end
+M.role_id = function()
+    return M.__role_id
+end
+M.user_data = function()
+    return M.__user_data
+end
+M.action = function()
+    return M.__action
+end
+M.filepath = function()
+    return M.__filepath
+end
+M.ext = function()
+    return M.__ext
+end
+return M

+ 34 - 0
target/fwutils/stopwatch.lua

@@ -0,0 +1,34 @@
+local M = {}
+
+M.tss = {}
+
+M.record = function(name)
+    if name == nil then
+        name = ""
+    end
+    table.insert(M.tss, {
+        name = name,
+        ts = fw_now_msec(),
+    })
+end
+
+M.print = function()
+
+    for i = 2, #M.tss do
+        local prev = M.tss[i - 1]
+        local curr = M.tss[i]
+        local diff = curr.ts - prev.ts
+        if diff < 1000 then
+            print(string.format("%s --> %dms", curr.name, diff))
+        else
+            print(string.format("%s --> %.2fs", curr.name, diff / 1000))
+        end
+    end
+    M.tss = {}
+
+end
+
+
+
+
+return M

+ 435 - 0
target/fwutils/template_engine.lua

@@ -0,0 +1,435 @@
+local utils = require("utils")
+local fw = require("fastweb")
+local config = require("fwutils.config")
+local request = require("fastweb.request")
+local response = require("fastweb.response")
+local cjson = require("cjson")
+-- 允许的扩展名
+local allowed_extensions = {
+    "shtml",
+    "html",
+    "js"
+}
+local template_engine_bc_this_role = nil
+local cfg = nil
+
+local M = {}
+
+function file_get_contents(filepath)
+    
+    local file, errmsg = io.open(fw.website_dir()..filepath, "r")
+    if not file then
+        err.server(errmsg)
+    end
+    local content = file:read("*a")
+    if content == nil or content == "" then
+        print("file_get_contents error: ",filepath)
+        return ""
+    end
+    local replaced,c2 = M.replace(content)
+    if replaced then
+        return c2
+    end
+    return content
+end
+function menu_top()
+    -- 当前请求路径
+    local request_path = request.filepath() or ""
+    -- 取配置
+    local menuData = require("fwutils.menu").get(string.format("%d",cfg.role_id()))
+    if not menuData then
+        return "<!-- no menu config -->"
+    end
+
+    -- 提取并排序主菜单
+    local menuItems = {}
+    for name, item in pairs(menuData) do
+        table.insert(menuItems, {
+            name = name,
+            item = item,
+            sort = item.sort or 0
+        })
+    end
+    table.sort(menuItems, function(a, b)
+        return a.sort > b.sort
+    end)
+
+    local html = [[
+
+  <ul class="navbar-nav me-auto mb-2 mb-lg-0">
+  ]]
+
+    -- 渲染主菜单
+    for _, entry in ipairs(menuItems) do
+        local name = entry.name
+        local item = entry.item
+        local icon = item.icon or ""
+        local path = item.path or "#"
+        local activeClass = ""
+
+        if item.children then
+            -- 有子菜单
+            local isMegamenu = item.megamenu and " dropdown-megamenu" or ""
+
+            -- 检查子菜单是否有激活
+            local hasActiveChild = false
+            local children = {}
+            for childName, childItem in pairs(item.children) do
+                table.insert(children, {
+                    name = childName,
+                    item = childItem,
+                    sort = childItem.sort or 0
+                })
+                if not hasActiveChild and (request_path == (childItem.path or "")) then
+                    hasActiveChild = true
+                end
+            end
+            table.sort(children, function(a, b)
+                return a.sort > b.sort
+            end)
+
+            -- 父菜单激活:当前页面是父菜单path,或是任一子菜单path
+            if request_path == path or hasActiveChild then
+                activeClass = "active-link"
+            end
+
+            html = html .. [[
+    <li class="nav-item dropdown ]] .. activeClass .. [[">
+      <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
+        <i class="]] .. icon .. [["></i> ]] .. name .. [[
+      </a>
+      <ul class="dropdown-menu]] .. isMegamenu .. [[">
+  ]]
+
+            -- 渲染子菜单
+            for _, child in ipairs(children) do
+                local childName = child.name
+                local childItem = child.item
+                local childPath = childItem.path or "#"
+
+                -- 子菜单不设置激活样式
+                html = html .. [[
+        <li>
+          <a class="dropdown-item" href="]] .. childPath .. [[">
+            <span>]] .. childName .. [[</span>
+          </a>
+        </li>
+  ]]
+            end
+
+            html = html .. [[
+      </ul>
+    </li>
+  ]]
+        else
+            -- 没有子菜单
+            -- 简单的“当前页面是否激活”检查
+            if request_path == path then
+                activeClass = "active-link"
+            end
+            html = html .. [[
+    <li class="nav-item ]] .. activeClass .. [[">
+      <a class="nav-link" href="]] .. path .. [[">
+        <i class="]] .. icon .. [["></i> ]] .. name .. [[
+      </a>
+    </li>
+  ]]
+        end
+    end
+
+    html = html .. [[
+  </ul>
+  ]]
+
+    return [=[
+    <nav class="navbar navbar-expand-lg">
+          <div class="container">
+            <div class="offcanvas offcanvas-end" id="MobileMenu">
+              <div class="offcanvas-header">
+                <h5 class="offcanvas-title semibold">Navigation</h5>
+                <button type="button" class="btn btn-danger btn-sm ms-auto" data-bs-dismiss="offcanvas">
+                  <i class="icon-clear"></i>
+                </button>
+              </div>
+    ]=]..html..[[
+            </div>
+          </div>
+        </nav>
+    ]]
+end
+function template(filepath)
+    local path = "/public/user/template/"..config.agent[user_agent_id()].template.."/"..filepath
+    return file_get_contents(path) 
+end
+
+function teacher_photos()
+    require("app.app")
+    local agent_id = user_agent_id()
+    local teacher = require("app.function.teacher")
+    local teacher_info = teacher.get_by_id(pint("id"))
+    if teacher_info == nil then
+        return ""
+    end
+    local photos = cjson.decode(teacher_info.photo)
+    local content = "<div><div class='home-img'><img src='".. teacher_info.avatar.."' class='img-fluid bg-img' alt=''></div></div>"
+    for i,v in ipairs(photos) do
+        if type(v) == "string" then
+            content = content..[[<div>
+                <div class="home-img">
+                    <img src="]]..v..[[" class="img-fluid bg-img" alt="">
+                </div>
+            </div>
+            ]] 
+        end
+    end
+    return content
+end
+-- 更新
+M.update = function(conn)
+    -- 查询权限表
+    local select = conn:select()
+    select:table("fw_template")
+    select:where_i32("enable","=",1)
+    local result = select:query()
+    local bc = {
+        public = {}
+    }
+    while result:next() do
+        local id = result:get("id")
+        local role_id = string.format("%d",result:get("role_id"))
+        local key = result:get("key")
+        local value = result:get("value")
+
+
+        if bc[role_id] == nil then
+            bc[role_id] = {}
+        end
+        if role_id == "0" then
+            bc["public"][key] = value
+        else
+            bc[role_id][key] = value
+        end
+    end
+    local code = "return " .. require("serpent").serialize(bc, {comment = false})
+    utils.save_file(fw.website_dir().."/"..(config.path.luabytecode:gsub("%.", "/")).."/template_engine_bc.lua",code)
+    return true
+end
+-- 处理
+-- @param static_content 静态内容
+-- @param cfg 配置
+-- @return 是否替换(TRUE则不需要继续处理,FALSE则继续处理)
+M.handle = function(__cfg)
+
+    local function has_ext(ext)
+        for _, v in ipairs(allowed_extensions) do
+            if v == ext then
+                return true
+            end
+        end
+        return false
+    end
+
+
+    cfg = __cfg
+    template_engine_bc_this_role = {
+        template = {
+            private = {},
+            public = {}
+        },
+    }
+
+
+    local ext = utils.ext(cfg.filepath())
+    local static_content = nil
+    if ext ~= nil  then
+        if not has_ext(ext) then
+            return false
+        end
+        -- 读取资源文件
+        static_content = utils.read_file(fw.website_dir()..cfg.filepath())
+        if static_content == nil or static_content == "" then
+            return false
+        end
+    else
+        -- 无需替换的扩展名
+        return false
+    end 
+    local template_engine_bc = require(config.path.luabytecode..".template_engine_bc")
+    template_engine_bc_this_role["template"]["private"] = template_engine_bc[string.format("%d",cfg.role_id())]
+    template_engine_bc_this_role["template"]["public"] = template_engine_bc["public"]
+    
+
+    local replaced,content = M.replace(static_content)
+    if replaced then
+        static_content = content
+    end
+    -- 执行函数
+    static_content, n = static_content:gsub("%${<<<%s*(.-)%s*>>>}", function(code)
+        -- 尝试编译代码(Lua 5.2+ 使用 load;Lua 5.1 可用 loadstring)
+        local chunk, errmsg = load(code)
+        if not chunk then
+            fw.throw_string(errmsg)
+        end
+        -- 使用 pcall 安全执行代码块
+        local status, result = pcall(chunk)
+        if not status then 
+            fw.throw_string(result)
+        end
+        -- 如果代码没有返回值,则替换为空字符串,否则转换成字符串返回
+        return tostring(result)
+    end)
+
+    if n > 0 then
+        replaced = true
+    end
+ 
+    if replaced then
+        if  ext == "shtml" or ext == "html" then
+            response.header("Content-Type","text/html")
+        elseif ext == "js" then
+            response.header("Content-Type","application/javascript")
+        end
+        response.send(static_content)
+        return true
+    end
+    return false
+end
+M.replace = function (content)
+    if content == nil or content == "" then
+        return false 
+    end
+    -- 支持多级kvs替换,如 ${people.age}
+    local function flatten_kvs(tbl, prefix, out)
+        if tbl == nil then
+            return {}
+        end
+        out = out or {}
+        prefix = prefix or ""
+        for k, v in pairs(tbl) do
+            local key = prefix ~= "" and (prefix .. "." .. k) or k
+            if type(v) == "table" then
+                flatten_kvs(v, key, out)
+            else
+                out[key] = v
+            end
+        end
+        return out
+    end
+
+    -- 先提取 content 中所有需要替换的占位符
+    -- 需要正确处理 ${<<<...>>>} 块:块内部的 ${...} 需要替换,但块本身不替换
+    local placeholders = {}
+    
+    -- 第一步:提取所有 ${<<<...>>>} 块,临时替换它们,并收集所有占位符
+    local blocks = {}
+    local block_index = 0
+    local temp_content = content:gsub("%${<<<%s*(.-)%s*>>>}", function(block_content)
+        block_index = block_index + 1
+        local placeholder = "${__TEMP_BLOCK_" .. block_index .. "__}"
+        -- 收集块内部的 ${...} 占位符
+        for ph in string.gmatch(block_content, "%${([^}]+)}") do
+            placeholders[ph] = true
+        end
+        blocks[block_index] = {
+            placeholder = placeholder,
+            content = block_content
+        }
+        return placeholder
+    end)
+    
+    -- 第二步:提取外部(不在 ${<<<...>>>} 块中)的 ${...} 占位符
+    for placeholder in string.gmatch(temp_content, "%${([^}]+)}") do
+        -- 忽略临时占位符
+        if not placeholder:match("^__TEMP_BLOCK_%d+__$") then
+            placeholders[placeholder] = true
+        end
+    end
+
+    -- print("PLACEHOLDERS:",cjson.encode(placeholders))
+    
+    -- 如果没有任何占位符,直接返回
+    if next(placeholders) == nil then
+        return false
+    end
+
+    -- 合并所有数据源到一个查找表中(只 flatten 一次)
+    local value_map = {}
+    
+    -- PUBLIC
+    local flat_kvs = flatten_kvs(template_engine_bc_this_role["template"]["public"])
+    for k, v in pairs(flat_kvs) do
+        value_map[k] = v
+    end
+    -- PRIVATE
+    flat_kvs = flatten_kvs(template_engine_bc_this_role["template"]["private"])
+    for k, v in pairs(flat_kvs) do
+        value_map[k] = v
+    end
+    -- REQUEST
+    flat_kvs = flatten_kvs(request.gets(), "request")
+    for k, v in pairs(flat_kvs) do
+        value_map[k] = v
+    end
+    -- TOKEN
+    flat_kvs = flatten_kvs(cfg.user_data(), "token")
+    for k, v in pairs(flat_kvs) do
+        value_map[k] = v
+    end
+
+    -- 转义函数:将 Lua 模式特殊字符转义为字面匹配
+    local function escape_pattern(str)
+        return str:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")
+    end
+    
+    -- 第三步:先处理 ${<<<...>>>} 块内部的占位符
+    local replaced = false
+    for i, block in ipairs(blocks) do
+        local block_content = block.content
+        for placeholder, _ in pairs(placeholders) do
+            if value_map[placeholder] ~= nil then
+                local escaped_placeholder = escape_pattern(placeholder)
+                local new_content, count = string.gsub(block_content, "%${" .. escaped_placeholder .. "}", tostring(value_map[placeholder]))
+                if count > 0 then
+                    block_content = new_content
+                    replaced = true
+                end
+            end
+        end
+        blocks[i].processed_content = block_content
+    end
+    
+    -- 第四步:替换外部的 ${...} 占位符(在临时内容中,此时块已被替换为临时占位符)
+    local n = 0
+    for placeholder, _ in pairs(placeholders) do
+        if value_map[placeholder] ~= nil then
+            local escaped_placeholder = escape_pattern(placeholder)
+            temp_content, n = string.gsub(temp_content, "%${" .. escaped_placeholder .. "}", tostring(value_map[placeholder]))
+            if n > 0 then
+                replaced = true
+            end
+        end
+    end
+    
+    -- 第五步:恢复 ${<<<...>>>} 块(使用处理后的内容)
+    for i, block in ipairs(blocks) do
+        local processed_block = "${<<<" .. blocks[i].processed_content .. ">>>}"
+        temp_content = temp_content:gsub(block.placeholder:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1"), processed_block)
+    end
+    content = temp_content
+
+    if replaced then
+        return true, content
+    end
+    return false
+end
+
+-- 检查内容中是否有TOKEN变量
+-- @return boolean
+M.hasToken = function()
+    if M.static_content == nil or M.static_content == "" then
+        return false
+    end
+    return M.static_content:match("%${token%.") ~= nil
+end
+
+return M

+ 92 - 0
target/fwutils/token.lua

@@ -0,0 +1,92 @@
+local config = require("fwutils.config")
+local codec = require("fastweb.codec")
+local utils = require("utils")
+local cjson = require("cjson")
+local redis_pool = require("redis.pool")
+local M = {}
+local function make_key(id,token)
+    if type(id) == "number" then
+        id = string.format("%d",id)
+    end
+    return config.server_id.."_token_"..id.."_"..token
+end
+M.__parse_token = function(token)
+    if token == nil or token == "" then
+        return false,"token is required"
+    end
+    local token_data = codec.aes_de(config.token.key,utils.hex_to_bytes(token),config.token.algorithm,config.token.mode)
+    if token_data == nil or token_data == "" then
+        return false,"token is invalid(a)"
+    end
+    local token_data_json = cjson.decode(utils.hex_to_bytes(token_data))
+    if token_data_json == nil or token_data_json.id == nil or token_data_json.id <= 0 then
+        return false,"token is invalid(no id)"
+    end
+    return true,token_data_json 
+end
+M.get = function(token)
+    -- 解析TOKEN
+    local result,token_data_json = M.__parse_token(token)
+    if not result then
+        return false,token_data_json
+    end
+     
+
+    -- 获取TOKEN数据
+    local conn = redis_pool.new(_G[config.db.redis_pool_name]):get()
+
+    local data = conn:get(make_key(token_data_json["id"],token))
+    if data ~= nil and data ~= "" then
+        return true,cjson.decode(data)
+    end
+    return false,"token is invalid"
+end
+M.set = function(token,data)
+    -- 解析TOKEN
+    local result,token_data_json = M.__parse_token(token)
+    if not result then
+        return false,token_data_json
+    end
+    if data == nil then
+        return false,"data is required"
+    end
+    local conn = redis_pool.new(_G[config.db.redis_pool_name]):get()
+    conn:setex(make_key(token_data_json["id"],token),config.token.expire,cjson.encode(data))
+    return true
+end
+M.del_by_id = function(id)
+    if id == nil or id <= 0 then
+        return false,"id is required"
+    end
+    local conn = redis_pool.new(_G[config.db.redis_pool_name]):get()
+    local keys = conn:keys(config.other.SERVER_ID.."_token_"..string.format("%d",id).."_*")
+    for _,key in ipairs(keys) do
+        conn:del(key)
+    end
+    return true
+end
+M.del_by_token = function(token)
+    -- 解析TOKEN
+    local result,token_data_json = M.__parse_token(token)
+    if not result then
+        return false,token_data_json
+    end
+    local conn = redis_pool.new(_G[config.db.redis_pool_name]):get()
+    conn:del(make_key(token_data_json["id"],token))
+    return true
+end
+M.create = function(id,data)
+    if id == nil or id <= 0 then
+        return false,"id is required"
+    end
+    local key_data = {
+        id = id,
+        create_time = fw.now_msec()
+    }
+    local token = codec.aes_en(config.token.key,cjson.encode(key_data),config.token.algorithm,config.token.mode)
+    if M.set(token,data) == false then
+        return false,"create token failed"
+    end 
+    return true,token
+end
+return M

+ 71 - 0
target/httpclient.lua

@@ -0,0 +1,71 @@
+local http = require("socket.http")
+local ltn12 = require("ltn12")
+local cjson = require("cjson")
+require("app.app")
+
+local M = {}
+
+M.get = function(url,headers)
+    local response_body = {}
+    local res, status_code, response_headers, status_text = http.request{
+        url = url,
+        method = "GET",
+        headers = headers,
+        sink = ltn12.sink.table(response_body)
+    } 
+
+    if not res then
+        return false,"err_http_get1,status_code:"..(status_code or "N/A")..",status_text:"..(status_text or "N/A")..",response_body:"..table.concat(response_body)
+    end
+    if status_code ~= 200 then
+        return false,"err_http_get2,status_code:"..(status_code or "N/A")..",status_text:"..(status_text or "N/A")..",response_body:"..table.concat(response_body)
+    end
+    return true,table.concat(response_body)
+end
+
+M.post = function(url,headers,body)
+    local response_body = {}
+    headers["Content-Length"] = tostring(#body)
+    local res, status_code, response_headers, status_text = http.request{
+        url = url,
+        method = "POST",
+        headers = headers,
+        source = ltn12.source.string(body),
+        sink = ltn12.sink.table(response_body)
+    }
+    if not res then
+        return false,"err_http_post,status_code:"..(status_code or "N/A")..",status_text:"..(status_text or "N/A")..",response_body:"..table.concat(response_body)
+    end
+    if status_code ~= 200 then
+        return false,"err_http_post,status_code:"..(status_code or "N/A")..",status_text:"..(status_text or "N/A")..",response_body:"..table.concat(response_body)
+    end
+    return true,table.concat(response_body)
+end
+M.delete = function(url,headers)
+    local response_body = {}
+    local res, status_code, response_headers, status_text = http.request{
+        url = url,
+        method = "DELETE",
+        headers = headers,
+        sink = ltn12.sink.table(response_body)
+    } 
+
+    if not res then
+        return false,"err_http_delete,status_code:"..(status_code or "N/A")..",status_text:"..(status_text or "N/A")..",response_body:"..table.concat(response_body)
+    end
+    if status_code ~= 200 then
+        return false,"err_http_delete,status_code:"..(status_code or "N/A")..",status_text:"..(status_text or "N/A")..",response_body:"..table.concat(response_body)
+    end
+    return true,table.concat(response_body)
+end
+-- 生成 A=B&C=D 的格式
+M.build_query = function(params)
+    local query = {}
+    for k, v in pairs(params) do
+        table.insert(query, k .. "=" .. v)
+    end
+    return table.concat(query, "&")
+end
+
+
+return M

+ 56 - 0
target/submail/sms.lua

@@ -0,0 +1,56 @@
+local M = {}
+local httpclient = require("fwutils.httpclient")
+local sms_url = "https://api-v4.mysubmail.com/sms/xsend"
+local sms_world_url = "https://api-v4.mysubmail.com/internationalsms/xsend"
+M.send = function(phone,appid,appkey,signature,template_code,vars)
+    
+    local data = {
+        appid = appid, 
+        signature = appkey,
+        to = phone,
+        project = template_code,
+        vars = cjson.encode(vars), 
+        sms_signature = signature,
+    }
+    local result,msg = httpclient.post(sms_url,{
+        ["Content-Type"] = "application/json",
+    },cjson.encode(data))
+    
+    if result then
+        local json = cjson.decode(msg)
+        if json.status == "success" then
+            return true
+        else
+            return false,json.msg
+        end
+    else
+        return false,msg
+    end
+end
+M.send_world = function(phone,appid,appkey,template_code,vars)
+
+    local data = {
+        appid = appid, 
+        signature = appkey,
+        to = phone,
+        project = template_code,
+        vars = cjson.encode(vars)
+
+    }
+    print(cjson.encode(data))
+    local result,msg = httpclient.post(sms_world_url,{
+        ["Content-Type"] = "application/json",
+    },cjson.encode(data))
+    
+    if result then
+        local json = cjson.decode(msg)
+        if json.status == "success" then
+            return true
+        else
+            return false,json.msg
+        end
+    else
+        return false,msg
+    end
+end
+return M 

+ 7 - 0
target/tencent/board.lua

@@ -0,0 +1,7 @@
+require("app.app")
+local M = {}
+
+
+
+
+return M

+ 77 - 0
target/tencent/board/generate_user_sig.lua

@@ -0,0 +1,77 @@
+--[[
+* Module:   GenerateUserSig (Lua version)
+* Function: Generate UserSig for Tencent Cloud IM/Board SDK
+* Reference: C++ implementation provided
+* Note: This implementation uses Lua's standard libraries and assumes the presence of 'openssl' and 'zlib' Lua modules.
+* Usage: require this module and call gen_user_sig(user_id, sdkappid, secretkey, expiretime)
+--]]
+require("app.app")
+local zlib = require("zlib")
+local base64 = require("app.module.base64")
+local M = {}
+
+-- Helper: base64 encode, then replace +, /, = as required by Tencent
+local function base64_url_encode(data)
+    local b64 = base64.encode(data)
+    -- 按照C++代码的字符替换规则
+    b64 = b64:gsub("+", "*"):gsub("/", "-"):gsub("=", "_")
+    return b64
+end
+ 
+-- Helper: HMAC-SHA256
+local function hmac_sha256(key, msg)
+    -- 确保参数都是字符串类型
+    return codec.hmac_sha256(tostring(key), tostring(msg))
+end
+
+-- Helper: hex string -> binary string
+local function hex_to_bin(hex)
+    return (hex:gsub("..", function(cc)
+        return string.char(tonumber(cc, 16))
+    end))
+end
+
+-- Helper: Generate the HMAC-SHA256 signature string
+local function gen_hmac_sig(user_id, sdkappid, curr_time, expire_time, secret_key)
+    local content = string.format(
+        "TLS.identifier:%s\nTLS.sdkappid:%d\nTLS.time:%d\nTLS.expire:%d\n",
+        user_id, sdkappid, curr_time, expire_time
+    )
+    -- 底层返回十六进制摘要,需转为二进制再做标准Base64
+    local sig_hex = hmac_sha256(secret_key, content)
+    local sig_bin = hex_to_bin(sig_hex)
+    return base64.encode(sig_bin)
+end
+
+-- Main: Generate the UserSig JSON, compress, and base64 encode
+local function gen_user_sig(user_id, sdkappid, secretkey, expiretime)
+    assert(user_id and user_id ~= "", "user_id must not be empty")
+    assert(tonumber(sdkappid) and tonumber(sdkappid) > 0, "sdkappid must be a positive integer")
+    assert(tonumber(expiretime) and tonumber(expiretime) > 0, "expiretime must be a positive integer")
+    assert(secretkey and secretkey ~= "", "secretkey must not be empty")
+
+    local curr_time = os.time()
+    -- 按官方JS示例:TLS.expire 使用持续时间(秒),不是绝对时间戳
+    local expire_time = expiretime
+
+    local sig = gen_hmac_sig(user_id, sdkappid, curr_time, expire_time, secretkey)
+
+    -- Compose JSON string (严格按照C++代码的格式和顺序)
+    local json = string.format(
+        '{"TLS.ver":"2.0","TLS.identifier":"%s","TLS.sdkappid":%d,"TLS.expire":%d,"TLS.time":%d,"TLS.sig":"%s"}',
+        user_id, sdkappid, expire_time, curr_time, sig
+    )
+
+    -- Compress with zlib (raw deflate) - 使用Z_BEST_SPEED压缩级别
+    local deflater = zlib.deflate()
+    local compressed, eof, bytes_in, bytes_out = deflater(json, "finish")
+
+    -- Base64 encode and replace chars as required
+    local user_sig = base64_url_encode(compressed)
+    return user_sig
+end
+
+-- Public API
+M.gen_user_sig = gen_user_sig
+
+return M

+ 150 - 0
target/tencent/cos.lua

@@ -0,0 +1,150 @@
+local tencent_cos = require("tencent_cos")
+local fw = require("fastweb")
+local utils = require("app.utils")
+local config = require("app.config")
+local M = {}
+-- 文件扩展名与目录的映射
+M.ext_dirpath = {
+    images = {
+        "jpg",
+        "jpeg",
+        "png",
+        "gif",
+        "bmp",
+        "webp",
+        "svg",
+        "ico",
+        "tif",
+        "tiff",
+    },
+    videos = {
+        "mp4",
+        "avi",
+        "mov",
+        "wmv",
+        "flv"
+    },
+    audio = {
+        "mp3",
+        "wav",
+        "ogg",
+        "aac",
+        "m4a",
+        "wma"
+    },
+    documents = {
+        "pdf",
+        "doc",
+        "docx",
+        "xls",
+        "xlsx",
+        "ppt",
+        "pptx"
+    }
+}
+
+function M.upload_data(config,object_name,remote_dir,data,ext)
+
+    local filename = fw.make_software_guid().."."..ext
+    local dirpath = ""
+
+    
+    for dir, ext_list in pairs(M.ext_dirpath) do
+        for _, ext_type_ext in ipairs(ext_list) do
+            if ext_type_ext == ext then
+                dirpath = dir
+                break
+            end
+        end
+        if dirpath ~= "" then
+            break
+        end
+    end
+    if dirpath == "" then
+        return false,"不支持的文件格式,ext: "..ext
+    end
+
+    if remote_dir == nil or remote_dir == "" then
+        remote_dir = ""
+    else
+        remote_dir = remote_dir.."/"
+    end
+
+
+    local cos = tencent_cos.new()
+
+
+    local local_file = fw.website_dir()..config.path.temp.."/"..filename
+    utils.save_file(local_file,data)
+
+    local cos_filepath = remote_dir..dirpath.."/"..filename
+    local result = cos:upfile(
+        config.appid,
+        config.endpoint,
+        config.secret_id,
+        config.secret_key,
+        object_name,
+        cos_filepath,
+        local_file
+    )
+    if result == "" then
+        return true,cos_filepath
+    end
+    return false,result
+end
+function M.upload_file(config,object_name,remote_dir,local_filepath,auto_remove)
+
+    if not utils.exists_file(local_filepath) then
+        return false,"文件不存在,local_filepath: "..local_filepath
+    end
+    local ext = utils.ext(local_filepath)
+    local filename = fw.make_software_guid().."."..ext
+
+    local dirpath = ""
+    
+    for dir, ext_list in pairs(M.ext_dirpath) do
+        for _, ext_type_ext in ipairs(ext_list) do
+            if ext_type_ext == ext then
+                dirpath = dir
+                break
+            end
+        end
+        if dirpath ~= "" then
+            break
+        end
+    end
+    if dirpath == "" then
+        return false,"不支持的文件格式,ext: "..ext
+    end
+
+    if remote_dir == nil or remote_dir == "" then
+        remote_dir = ""
+    else
+        remote_dir = remote_dir.."/"
+    end
+
+    local cos_filepath = remote_dir .. dirpath.."/"..filename
+    local cos = tencent_cos.new()
+
+
+    local result = cos:upfile(
+        config.appid,
+        config.endpoint,
+        config.secret_id,
+        config.secret_key,
+        object_name,
+        cos_filepath,
+        local_filepath
+    )
+    if result == "" then
+        if auto_remove then
+            utils.delete_file(local_filepath)
+        end
+        return true,cos_filepath
+    end
+    return false,result
+end
+
+
+
+return M

+ 286 - 0
target/tencent/qywx.lua

@@ -0,0 +1,286 @@
+local http = require("fwutils.httpclient")
+local codec = require("fastweb.codec")
+local base64 = require("fwutils.base64")
+local openssl = require("openssl")
+local M = {}
+
+local url_get_access_token = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
+local url_send_message = "https://qyapi.weixin.qq.com/cgi-bin/message/send"
+local url_get_userid_by_mobile = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserid"
+local url_get_userids = "https://qyapi.weixin.qq.com/cgi-bin/user/list_id"
+
+
+-- 验证签名
+M.verify_signature = function(token,timestamp,nonce,echostr)
+    local params = {token, timestamp, nonce, echostr}
+    table.sort(params, function(a, b) return tostring(a) < tostring(b) end)
+    return string.lower(codec.sha1(table.concat(params)))
+end
+
+-- 解密数据
+M.decrypt_data = function(qywx_aeskey,data)
+
+    -- 16随机数+4字节消息长度+消息体+接收ID
+    local function parse_decrypted_msg(data)
+        -- data: string, decrypted_echostr
+        if #data < 20 then
+            return nil, "data too short"
+        end
+        -- 跳过16字节随机数
+        local msg_len_bytes = data:sub(17, 20)
+        local b1, b2, b3, b4 = msg_len_bytes:byte(1, 4)
+        local msg_len = b1 * 2^24 + b2 * 2^16 + b3 * 2^8 + b4
+        local msg_start = 21
+        local msg_end = 20 + msg_len
+        local msg = data:sub(msg_start, msg_end)
+        return msg
+    end
+ 
+    local cipher = openssl.cipher.new("aes-256-cbc")
+    cipher:init(base64.decode(qywx_aeskey), "0123456789abcdef", false)
+    local plaintext = cipher:update(data)
+    plaintext =  parse_decrypted_msg(plaintext)
+    return plaintext
+end
+
+
+-- 获取access_token
+M.get_access_token = function(corpid,corpsecret)
+
+    local url = url_get_access_token .. "?corpid=" .. corpid .. "&corpsecret=" .. corpsecret
+    local result,err = http.get(url,{
+        ["Content-Type"] = "application/json"
+    })
+    if not result then
+        return false,err
+    end
+    local data = cjson.decode(err)
+    if data.errcode == 0 then
+        return true,data.access_token
+    end
+    return false,data.errmsg
+end
+-- 获取userid
+M.get_userid_by_mobile = function(access_token,mobile)
+    local url = url_get_userid_by_mobile .. "?access_token=" .. access_token .. "&mobile=" .. mobile
+    local result,err = http.post(url,{
+        ["Content-Type"] = "application/json"
+    },cjson.encode({
+        mobile = mobile
+    }))
+    if not result then
+        return false,err
+    end
+    local data = cjson.decode(err)
+    if data.errcode == 0 then
+        return true,data.userid
+    end
+    return false,data.errmsg
+end
+-- 获取用户列表
+M.get_userids = function(access_token)
+    local url = url_get_userids .. "?access_token=" .. access_token
+    local result,err = http.post(url,{
+        ["Content-Type"] = "application/json"
+    },cjson.encode({
+        limit = 1000
+    }))
+    if not result then
+        return false,err
+    end
+    local data = cjson.decode(err)
+    if data.errcode == 0 then
+        return true,data.dept_user
+    end
+    return false,data.errmsg
+end
+-- 上传临时文件
+M.upload_temp_file = function(access_token,data,type_str)
+    local function make_upload_temp_data(data)
+        local boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"
+        local body = ""
+        
+        local filename = ""
+        if type_str == "image" then
+            filename = tostring(os.time())..".png"
+        elseif type_str == "video" then
+            filename = tostring(os.time())..".mp4"
+        end
+
+        -- 构造 multipart 数据
+        body = body .. "--" .. boundary .. "\r\n"
+        body = body .. "Content-Disposition: form-data; name=\"media\"; filename=\""..filename.."\"\r\n"
+        body = body .. "Content-Type: application/octet-stream\r\n\r\n" .. data .. "\r\n"
+        body = body .. "--" .. boundary .. "--\r\n"
+        
+        return body
+    end
+    local body = make_upload_temp_data(data)
+    local url = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=" .. access_token.."&type="..type_str
+    local result,err = http.post(url,{
+        ["Content-Type"] = "multipart/form-data; boundary=--WebKitFormBoundary7MA4YWxkTrZu0gW",
+        ["Content-Length"] = #body
+    },body)
+    if not result then
+        return false,err
+    end
+    local data = cjson.decode(err)
+    if data.errcode == 0 then
+        return true,data.media_id
+    end 
+    return false,data.errmsg
+end
+-- 发送消息 
+M.send_message = function(access_token,userid,agentid,message)
+    local url = url_send_message .. "?access_token=" .. access_token
+    local result,err = http.post(url,{
+        ["Content-Type"] = "application/json"
+    },cjson.encode({
+        touser = userid,
+        msgtype = "text",
+        agentid = agentid,
+        text = {
+            content = message
+        }
+    }))
+    if not result then
+        return false,err
+    end
+    local data = cjson.decode(err)
+    if data.errcode == 0 then
+        return true
+    end 
+    return false,data.errmsg
+end
+-- 发送图片消息
+M.send_image_message = function(access_token,userid,agentid,image_filepath)
+    local file = io.open(image_filepath, "rb")
+    if not file then
+        return false, "cannot open file: " .. tostring(image_filepath)
+    end
+    local data = file:read("*a")
+    file:close()
+    local ok, media_id = M.upload_temp_file(access_token, data,"image")
+    if not ok then
+        return false, "upload_temp_file_failed:"..media_id
+    end
+    local url = url_send_message .. "?access_token=" .. access_token
+    local result, err = http.post(url, {
+        ["Content-Type"] = "application/json"
+    }, cjson.encode({
+        touser = userid,
+        msgtype = "image",
+        agentid = agentid,
+        image = {
+            media_id = media_id,
+        }
+    }))
+    if not result then
+        return false, err
+    end
+    local resp = cjson.decode(err)
+    if resp.errcode == 0 then
+        return true
+    end
+    return false, resp.errmsg
+end
+-- 发送视频消息
+M.send_video_message = function(access_token,userid,agentid,title,video_filepath)
+    local file = io.open(video_filepath, "rb")
+    if not file then
+        return false, "cannot open file: " .. tostring(video_filepath)
+    end
+    local data = file:read("*a")
+    file:close()
+    local ok, media_id = M.upload_temp_file(access_token, data,"video")
+    if not ok then
+        return false, "upload_temp_file_failed:"..media_id
+    end
+    local url = url_send_message .. "?access_token=" .. access_token
+    local result, err = http.post(url, {
+        ["Content-Type"] = "application/json"
+    }, cjson.encode({
+        touser = userid,
+        msgtype = "video",
+        agentid = agentid,
+        video = {
+            media_id = media_id,
+            title = title
+        }
+    }))
+    if not result then
+        return false, err
+    end
+    local resp = cjson.decode(err)
+    if resp.errcode == 0 then
+        return true
+    end
+    return false, resp.errmsg
+end
+-- 发送卡片消息
+M.send_card_message = function(access_token,userid,agentid,message)
+    local url = url_send_message .. "?access_token=" .. access_token
+    local result,err = http.post(url,{
+        ["Content-Type"] = "application/json"
+    },cjson.encode({
+        touser = userid,
+        msgtype = "news",
+        agentid = agentid,
+        news = {
+            articles = {
+                message
+            }
+        }
+    }))
+    if not result then
+        return false,err
+    end
+    local data = cjson.decode(err)
+    if data.errcode == 0 then
+        return true
+    end 
+    return false,data.errmsg
+end
+-- 发送卡片消息
+M.send_template_card_message = function(access_token,userid,agentid,message)
+    local url = url_send_message .. "?access_token=" .. access_token
+    local result,err = http.post(url,{
+        ["Content-Type"] = "application/json"
+    },cjson.encode({
+        touser = userid,
+        msgtype = "template_card",
+        agentid = agentid,
+        template_card = message
+    }))
+    if not result then
+        return false,err
+    end
+    local data = cjson.decode(err)
+    if data.errcode == 0 then
+        return true
+    end 
+    return false,data.errmsg
+end
+-- 发送MMD消息
+M.send_markdown_message = function(access_token,userid,agentid,message)
+    local url = url_send_message .. "?access_token=" .. access_token
+    local result,err = http.post(url,{
+        ["Content-Type"] = "application/json"
+    },cjson.encode({
+        touser = userid,
+        msgtype = "markdown",
+        agentid = agentid,
+        markdown = {
+            content = message 
+        }
+    }))
+    if not result then
+        return false,err
+    end
+    local data = cjson.decode(err)
+    if data.errcode == 0 then
+        return true
+    end 
+    return false,data.errmsg
+end
+return M

+ 218 - 0
target/tencent/wxofficial.lua

@@ -0,0 +1,218 @@
+require("app.app")
+local http = require("socket.http")
+local ltn12 = require("ltn12")
+local httpclient = require("fwutils.httpclient")
+local cache = require("fwutils.cache")
+
+
+local M = {}
+
+-- access_token 的 url
+M.url_get_access_token = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"
+
+-- 获取用户access_token的url
+M.url_get_user_access_token = "https://api.weixin.qq.com/sns/oauth2/access_token?grant_type=authorization_code"
+
+-- 获取用户信息的url
+M.url_get_user_info = "https://api.weixin.qq.com/sns/userinfo?lang=zh_CN"
+-- 授权地址
+M.url_auth = "https://open.weixin.qq.com/connect/oauth2/authorize"
+-- 模板消息
+M.url_template_message = "https://api.weixin.qq.com/cgi-bin/message/template/send"
+-- 获取 access_token
+M.get_access_token = function(appid, appsecret)
+    -- local value = cache.get_json("wxofficial_info")
+    -- if value then 
+    --     -- 是否距离过期至少还有5分钟
+    --     if value.expires_seconds > os.time() + 300 then
+    --         return true, value.access_token
+    --     end
+    -- end
+
+    -- 如果 appid 和 appsecret 为空,则返回 nil
+    if appid == nil or appid == "" then 
+        return false, "appid 为空"
+    end
+    if appsecret == nil or appsecret == "" then
+        return false, "appsecret 为空"
+    end
+
+
+
+    -- 先检查有没有过期
+    local cache_data = cache.get_json(tostring("wxofficial_info_"..appid))
+    if cache_data then
+        if cache_data.update_time and cache_data.access_token then
+            -- 检查是否过期
+            if os.time() - cache_data.update_time < 7200 then
+                return true,cache_data.access_token
+            end
+        end
+    end
+
+
+
+    local result, data = httpclient.get(M.url_get_access_token .. "&appid=" .. appid .. "&secret=" .. appsecret)
+    if not result then
+        return false, data
+    end
+
+    local body = cjson.decode(data)
+    if body.errcode ~= nil and body.errcode ~= 0 then
+        return false, body.errmsg
+    end
+
+ 
+    -- 更新
+    cache.set_json(tostring("wxofficial_info_"..appid),{
+        update_time = os.time(),
+        access_token = body.access_token
+    })
+    return true,body.access_token
+
+
+end
+
+-- 获取用户access_token
+M.get_user_access_token = function(appid, appsecret, code)
+    local result, data = httpclient.get(M.url_get_user_access_token .. "&appid=" .. appid .. "&secret=" .. appsecret .. "&code=" .. code)
+    if not result then
+        return false, data
+    end
+
+    local body = cjson.decode(data)
+    if body.errcode ~= nil and body.errcode ~= 0 then
+        return false, body.errmsg
+    end
+    return true, body
+end
+
+-- 获取用户信息
+M.get_user_info = function(access_token, openid)
+    local url = M.url_get_user_info .. "&access_token=" .. access_token .. "&openid=" .. openid
+    local result, data = httpclient.get(url)
+    if not result then
+        return false, data
+    end
+
+    local body = cjson.decode(data)
+    if body.errcode ~= nil and body.errcode ~= 0 then
+        return false, body.errmsg
+    end
+    return true, body
+end
+
+-- 生成授权地址
+M.generate_auth_url = function(appid, redirect_uri)
+    return M.url_auth .. "?appid=" .. appid .. "&redirect_uri=" .. redirect_uri .. "&response_type=code&scope=snsapi_userinfo&state=1#wechat_redirect"
+end
+
+-- 发送模板消息
+M.send_template_message = function(appid, appsecret, openid, template_id, jump_url, data)
+    local ok, access_token = M.get_access_token(appid, appsecret)
+    if not ok then
+        return false, access_token
+    end
+    local url = M.url_template_message .. "?access_token=" .. access_token
+
+    local body = {
+        touser = openid, 
+        template_id = template_id,
+        url = jump_url,
+        data = data,
+        appid = appid
+    }
+    local result, response = httpclient.post(url, {
+        ["Content-Type"] = "application/json"
+    }, cjson.encode(body))
+    
+    if not result then
+        return false, response
+    end
+    local res_json = cjson.decode(response)
+    if res_json.errcode ~= 0 then
+        return false, res_json.errmsg
+    end
+    return true
+end
+
+
+-- 生成一次性订阅确认地址
+M.generate_subscribe_confirm_url = function(appid, scene, template_id, redirect_uri, reserved)
+    return "https://mp.weixin.qq.com/mp/subscribemsg?action=get_confirm&appid=" .. appid .. "&scene=" .. scene .. "&template_id=" .. template_id .. "&redirect_url=" .. redirect_uri .. "&#wechat_redirect"
+end
+
+--------------------------------------------------------------------------------
+-- 以下为新增的部分,用于微信JS SDK 配置
+
+-- 生成随机字符串函数
+local function generate_nonce_str(length)
+    --math.randomseed(os.time() + math.random())  -- 增加随机种子
+    local chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
+    local res = {}
+    for i = 1, (length or 16) do
+        local rand = math.random(1, #chars)
+        res[#res+1] = string.sub(chars, rand, rand)
+    end
+    return table.concat(res)
+end
+
+-- 获取 jsapi_ticket
+M.get_jsapi_ticket = function(appid, appsecret)
+    local ticket_info = cache.get_json("wxjsapi_ticket")
+    if ticket_info then
+        if ticket_info.expires > os.time() + 300 then
+            return true, ticket_info.ticket
+        end
+    end
+
+    local ok, access_token = M.get_access_token(appid, appsecret)
+    if not ok then
+        return false, access_token
+    end
+
+    local ticket_url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" .. access_token .. "&type=jsapi"
+    local result, data = httpclient.get(ticket_url)
+    if not result then
+        return false, data
+    end
+
+    local body = cjson.decode(data)
+    if body.errcode ~= 0 then
+        return false, body.errmsg
+    end
+
+    cache.set_json("wxjsapi_ticket", {
+        ticket = body.ticket,
+        expires = os.time() + body.expires_in
+    })
+    return true, body.ticket
+end
+
+-- 获取微信JS SDK所需的配置参数
+-- 参数 current_url 必须是当前网页的完整 URL(不包含#号后的部分)
+M.get_js_sdk_config = function(appid, appsecret, current_url)
+    local ok, jsapi_ticket = M.get_jsapi_ticket(appid, appsecret)
+    if not ok then
+        return false, jsapi_ticket..",err"
+    end
+
+    local nonceStr = generate_nonce_str(16)
+    local timestamp = os.time()
+
+    -- 拼接待签名字符串,注意顺序及格式严格按照微信要求
+    local str = "jsapi_ticket=" .. jsapi_ticket .. "&noncestr=" .. nonceStr .. "&timestamp=" .. timestamp .. "&url=" .. current_url
+    -- 计算 SHA1 签名(需在 OpenResty 下使用 ngx.sha1_bin 和 resty.string.to_hex)
+    local signature = codec.sha1(str)
+
+    return true, {
+        appId = appid,
+        timestamp = timestamp,
+        nonceStr = nonceStr,
+        signature = signature
+    }
+end
+
+--------------------------------------------------------------------------------
+
+return M

+ 101 - 0
target/tencent/wxpay.lua

@@ -0,0 +1,101 @@
+local http = require("fwutils.httpclient")
+
+local M = {}
+-- 微信支付统一下单
+local url_pay = "https://api.mch.weixin.qq.com/pay/unifiedorder"
+
+
+-- 纯文本方式取中间内容(不使用正则)
+function extract_between(text, start_str, end_str)
+    local start_pos = string.find(text, start_str, 1, true)
+    if not start_pos then return nil end
+
+    local from = start_pos + #start_str
+    local end_pos = string.find(text, end_str, from, true)
+    if not end_pos then return nil end
+
+    return string.sub(text, from, end_pos - 1)
+end
+
+
+-- 验签
+M.sign = function(data,key)
+    local sign_str = ""
+    local keys = {}
+    for k,_ in pairs(data) do
+        table.insert(keys, k)
+    end
+    table.sort(keys)
+    for i,k in ipairs(keys) do
+        if i > 1 then
+            sign_str = sign_str .. "&"
+        end
+        sign_str = sign_str .. k .. "=" .. data[k]
+    end
+    sign_str = sign_str .. "&key=" .. key
+    return string.upper(codec.md5(sign_str))
+end
+
+-- 微信支付统一下单
+M.unified_order = function(appid,mch_id,openid,key,body,out_trade_no,total_fee,notify_url)
+    function xml_make(data)
+        local xml_data = "<xml>"
+        for k,v in pairs(data) do
+            xml_data = xml_data .. "<" .. k .. ">" .. v .. "</" .. k .. ">"
+        end
+        xml_data = xml_data .. "</xml>"
+        return xml_data
+    end
+    local data = {
+        appid = appid,
+        body = body,
+        mch_id = mch_id,
+        nonce_str = fw.make_software_guid(),
+        notify_url = notify_url,
+        openid = openid,
+        out_trade_no = out_trade_no,
+        spbill_create_ip = request.remote_ipaddress(),
+        total_fee = total_fee,
+        trade_type = "JSAPI",
+    }
+    data.sign = M.sign(data,key)
+
+    -- 生成XML
+    local xml_data = xml_make(data)
+    --  print("================xml_request=================")
+    --  print(appid,"|",mch_id,"|",openid,"|",key,"|",body,"|",out_trade_no,"|",total_fee,"|",notify_url)
+    --  print(xml_data) 
+    
+    local result,res = http.post(url_pay,{
+        ["Content-Type"] = "text/xml", 
+    }, xml_data)
+    if not result then 
+        return false,"request error"..res
+    end
+    --  print("============res=============")
+    --  print(res)
+
+    -- 取中间文本
+    local result_code = extract_between(res,"<result_code><![CDATA[","]]></result_code>")
+    if result_code ~= "SUCCESS" then
+        return false,extract_between(res,"<err_code_des><![CDATA[","]]></err_code_des>")
+    end
+
+    local prepay_id = extract_between(res,"<prepay_id><![CDATA[","]]></prepay_id>")
+
+    local result_data = {
+        appId = appid,
+        nonceStr = fw.make_software_guid(), 
+        package = "prepay_id=" .. prepay_id,
+        signType = "MD5",
+        timeStamp =  string.format("%d",os.time())
+    }
+    result_data["paySign"] = M.sign(result_data,key)
+    return true,result_data
+end
+
+
+
+return M
+
+

+ 286 - 0
target/utils.lua

@@ -0,0 +1,286 @@
+local lfs = require("lfs")
+
+local M = {}
+
+
+-- 创建目录
+M.create_dir = function(dirpath)
+    -- 检查操作系统类型
+    local os_type = package.config:sub(1,1)
+    if os_type == "\\" then
+        -- Windows 系统
+        os.execute("mkdir \"" .. dirpath:gsub("/", "\\") .. "\" /p")
+    else
+        -- Unix/Linux/Mac 系统
+        os.execute("mkdir -p \"" .. dirpath .. "\"")
+    end
+end
+-- 转换为整数
+M.tointeger = function(data)
+    local num = tonumber(data)
+    if num == nil then
+        return nil
+    end
+    return math.floor(num)
+end
+-- 转为时间戳
+M.to_timestamp = function(time_str,pattern)
+    if pattern == nil then
+        pattern = "(%d+)%-(%d+)%-(%d+)%s+(%d+):(%d+):(%d+)"
+    end
+    local y, m, d, h, min, s = time_str:match(pattern)
+    local timestamp = os.time({
+        year = tonumber(y),
+        month = tonumber(m),
+        day = tonumber(d),
+        hour = tonumber(h),
+        min = tonumber(min),
+        sec = tonumber(s)
+    })
+    return timestamp 
+end
+-- 取扩展名
+M.ext = function(filepath)
+    -- 取扩展名
+    return string.match(filepath,"%.([^.]+)$")
+end
+-- 复制文件
+-- 增加第三个参数 replace,是否替换目标文件,默认为 false
+M.copy_file = function(src, dst, replace)
+    replace = replace or false
+    -- 检查目标文件是否存在
+    local dst_file = io.open(dst, "r")
+    if dst_file ~= nil then
+        dst_file:close()
+        if not replace then
+            return true
+        end
+    end
+    -- 打开源文件
+    local file = io.open(src, "rb")
+    if not file then
+        print("ERR 2,src:",src)
+        return false
+    end
+    local content = file:read("*all")
+    file:close()
+    -- 写入目标文件
+    local file = io.open(dst, "wb")
+    if not file then
+        print("ERR 3") 
+        return false
+    end
+    file:write(content)
+    file:close()
+    return true
+end
+-- 取路径文件名(带扩展名)
+M.filename = function(filepath)
+    return string.match(filepath,"[^/]+$")
+end
+-- 删除文件
+M.delete_file = function(filepath)
+    if os.remove(fw.website_dir()..filepath) ~= true then
+        return false
+    end
+    return true
+end
+-- 读取文件内容
+M.read_file = function(filepath)
+    local file = io.open(filepath, "rb")
+    if not file then
+        return nil, "无法打开文件: " .. tostring(filepath)
+    end
+    local content = file:read("*all")
+    file:close()
+    return content
+end
+
+-- 保存内容到文件
+M.save_file = function(filepath, content)
+    local file = io.open(filepath, "wb")
+    if not file then
+        return false, "无法打开文件: " .. tostring(filepath)
+    end
+    file:write(content)
+    file:close()
+    return true
+end
+-- 是否存在文件
+M.exists_file = function(filepath)
+    local file = io.open(filepath, "rb")
+    if file then
+        file:close()
+        return true
+    else
+        return false
+    end
+end
+-- 取近N个月时间
+M.recent_months = function(n)
+    local mons = {}
+    local function format_time(y, m, d, h, i, s)
+        return string.format("%04d-%02d-%02d %02d:%02d:%02d", y, m, d, h, i, s)
+    end
+    local now = os.time()
+    local cur = os.date("*t", now)
+    for i = n-1, 0, -1 do
+        local year = cur.year
+        local month = cur.month - i
+        while month <= 0 do
+            month = month + 12
+            year = year - 1
+        end
+        -- 获取该月第一天与最后一天
+        local first_day = format_time(year, month, 1, 0, 0, 0)
+        local next_month = month + 1
+        local next_year = year
+        if next_month > 12 then
+            next_month = 1
+            next_year = year + 1
+        end
+        -- next_month 1号的前一天就是当前月最后一天
+        local last_day_ts = os.time{year=next_year, month=next_month, day=1, hour=0, min=0, sec=0} - 1
+        local last_day_tm = os.date("*t", last_day_ts)
+        local last_day = format_time(last_day_tm.year, last_day_tm.month, last_day_tm.day, 23, 59, 59)
+        local month_str = string.format("%04d-%02d", year, month)
+        table.insert(mons, month_str)
+    end
+    return mons
+end
+M.recent_months2 = function(n)
+    local mons = {}
+    local function format_time(y, m, d, h, i, s)
+        return string.format("%04d-%02d-%02d %02d:%02d:%02d", y, m, d, h, i, s)
+    end
+    local now = os.time()
+    local cur = os.date("*t", now)
+    for i = n-1, 0, -1 do
+        local year = cur.year
+        local month = cur.month - i
+        while month <= 0 do
+            month = month + 12
+            year = year - 1
+        end
+        -- 获取该月第一天与最后一天
+        local first_day = format_time(year, month, 1, 0, 0, 0)
+        local next_month = month + 1
+        local next_year = year
+        if next_month > 12 then
+            next_month = 1
+            next_year = year + 1
+        end
+        -- next_month 1号的前一天就是当前月最后一天
+        local last_day_ts = os.time{year=next_year, month=next_month, day=1, hour=0, min=0, sec=0} - 1
+        local last_day_tm = os.date("*t", last_day_ts)
+        local last_day = format_time(last_day_tm.year, last_day_tm.month, last_day_tm.day, 23, 59, 59)
+        local month_str = string.format("%04d-%02d", year, month)
+        table.insert(mons, {
+            month = month_str,
+            start_time = first_day,
+            end_time = last_day
+        })
+    end
+    return mons
+end
+-- 十六进制转字节数组
+M.hex_to_bytes = function(hex_str)
+    -- 移除所有非十六进制字符
+    hex_str = hex_str:gsub("[^%x]", ""):upper()
+    
+    -- 补0使长度为偶数
+    if #hex_str % 2 == 1 then
+        hex_str = "0" .. hex_str
+    end
+    
+    -- 使用 gsub 一次性转换
+    return (hex_str:gsub("(%x%x)", function(hex)
+        return string.char(tonumber(hex, 16))
+    end))
+end
+--[[
+    还原 Redis 字符串(去转义)
+    @param value 被转义的字符串
+    @return 返回还原后的字符串
+]]
+M.unescape_value = function(value)
+    if value == nil then
+        return nil
+    end
+    local str = tostring(value)
+    -- 如果是用双引号包住的,去掉包裹并恢复转义
+    if #str >= 2 and string.sub(str,1,1) == "\"" and string.sub(str,-1,-1) == "\"" then
+        str = string.sub(str,2,-2)
+        str = string.gsub(str, "\\\"", "\"")
+        str = string.gsub(str, "\\\\", "\\")
+        return str
+    end
+    return str
+end
+-- 是否为静态资源扩展名
+M.is_static_ext_not_html = function(ext)
+    local static_exts = {
+        "jpg",
+        "jpeg",
+        "png",
+        "gif",
+        "bmp",
+        "ico",
+        "pdf",
+        "doc",
+        "docx",
+        "xls",
+        "xlsx",
+        "ppt",
+        "pptx",
+        "txt",
+        "css",
+        "js",
+        "json",
+        "xml",
+        "yaml",
+        "yml",
+        "zip",
+        "rar",
+        "7z",
+        "tar",
+        "gz",
+        "bz2",
+        "xz",
+        "mp3",
+        "wav",
+        "ogg",
+        "aac",
+        "m4a",
+        "mp4",
+        "avi",
+        "mov",
+        "wmv",
+        "flv",
+        "webm",
+        "mkv",
+        "avi",
+        "mov",
+        "wmv",
+        "flv",
+        "webm",
+        "mkv",
+    }
+    for _, v in ipairs(static_exts) do
+        if v == ext then
+            return true
+        end
+    end
+    return false
+end
+-- 遍历目录
+M.traverse_dir = function(dirpath)
+    local files = {}
+    for file in lfs.dir(dirpath) do
+        if file ~= "." and file ~= ".." then
+            table.insert(files, file)
+        end
+    end
+    return files
+end
+return M