首次提交

This commit is contained in:
a158
2026-01-08 21:58:41 +08:00
parent f5e5f312a2
commit 53c171d42d
23 changed files with 2487 additions and 0 deletions

5
.gitignore vendored Normal file
View File

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

2
build.sh Normal file
View File

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

36
target/aliyun/ai.lua Normal file
View File

@@ -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
target/aliyun/email.lua Normal file
View File

@@ -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
target/base64.lua Normal file
View File

@@ -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
target/cache.lua Normal file
View File

@@ -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
target/fwutils/acl.lua Normal file
View File

@@ -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
target/fwutils/config.lua Normal file
View File

@@ -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
target/fwutils/init.lua Normal file
View File

@@ -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
target/fwutils/menu.lua Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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+ 使用 loadLua 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
target/fwutils/token.lua Normal file
View File

@@ -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
target/httpclient.lua Normal file
View File

@@ -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
target/submail/sms.lua Normal file
View File

@@ -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
target/tencent/board.lua Normal file
View File

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

View File

@@ -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
target/tencent/cos.lua Normal file
View File

@@ -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
target/tencent/qywx.lua Normal file
View File

@@ -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

View File

@@ -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
target/tencent/wxpay.lua Normal file
View File

@@ -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
target/utils.lua Normal file
View File

@@ -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