首次提交
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
################################################################################
|
||||||
|
# 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
/.vs
|
||||||
36
target/aliyun/ai.lua
Normal file
36
target/aliyun/ai.lua
Normal 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
191
target/aliyun/email.lua
Normal 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
52
target/base64.lua
Normal 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
40
target/cache.lua
Normal 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
117
target/fwutils/acl.lua
Normal 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
22
target/fwutils/config.lua
Normal 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
41
target/fwutils/init.lua
Normal 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
104
target/fwutils/menu.lua
Normal 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
|
||||||
64
target/fwutils/request_config.lua
Normal file
64
target/fwutils/request_config.lua
Normal 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
|
||||||
34
target/fwutils/stopwatch.lua
Normal file
34
target/fwutils/stopwatch.lua
Normal 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
|
||||||
435
target/fwutils/template_engine.lua
Normal file
435
target/fwutils/template_engine.lua
Normal 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+ 使用 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
target/fwutils/token.lua
Normal file
92
target/fwutils/token.lua
Normal 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
71
target/httpclient.lua
Normal 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
56
target/submail/sms.lua
Normal 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
7
target/tencent/board.lua
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
require("app.app")
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return M
|
||||||
77
target/tencent/board/generate_user_sig.lua
Normal file
77
target/tencent/board/generate_user_sig.lua
Normal 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
150
target/tencent/cos.lua
Normal 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
286
target/tencent/qywx.lua
Normal 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
|
||||||
218
target/tencent/wxofficial.lua
Normal file
218
target/tencent/wxofficial.lua
Normal 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 .. "×tamp=" .. 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
101
target/tencent/wxpay.lua
Normal 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
286
target/utils.lua
Normal 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
|
||||||
Reference in New Issue
Block a user