From 53c171d42de41f372032e2c64a7a1a102ec0ce4f Mon Sep 17 00:00:00 2001 From: a158 Date: Thu, 8 Jan 2026 21:58:41 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A6=96=E6=AC=A1=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 + build.sh | 2 + target/aliyun/ai.lua | 36 ++ target/aliyun/email.lua | 191 +++++++++ target/base64.lua | 52 +++ target/cache.lua | 40 ++ target/fwutils/acl.lua | 117 ++++++ target/fwutils/config.lua | 22 ++ target/fwutils/init.lua | 41 ++ target/fwutils/menu.lua | 104 +++++ target/fwutils/request_config.lua | 64 +++ target/fwutils/stopwatch.lua | 34 ++ target/fwutils/template_engine.lua | 435 +++++++++++++++++++++ target/fwutils/token.lua | 92 +++++ target/httpclient.lua | 71 ++++ target/submail/sms.lua | 56 +++ target/tencent/board.lua | 7 + target/tencent/board/generate_user_sig.lua | 77 ++++ target/tencent/cos.lua | 150 +++++++ target/tencent/qywx.lua | 286 ++++++++++++++ target/tencent/wxofficial.lua | 218 +++++++++++ target/tencent/wxpay.lua | 101 +++++ target/utils.lua | 286 ++++++++++++++ 23 files changed, 2487 insertions(+) create mode 100644 .gitignore create mode 100644 build.sh create mode 100644 target/aliyun/ai.lua create mode 100644 target/aliyun/email.lua create mode 100644 target/base64.lua create mode 100644 target/cache.lua create mode 100644 target/fwutils/acl.lua create mode 100644 target/fwutils/config.lua create mode 100644 target/fwutils/init.lua create mode 100644 target/fwutils/menu.lua create mode 100644 target/fwutils/request_config.lua create mode 100644 target/fwutils/stopwatch.lua create mode 100644 target/fwutils/template_engine.lua create mode 100644 target/fwutils/token.lua create mode 100644 target/httpclient.lua create mode 100644 target/submail/sms.lua create mode 100644 target/tencent/board.lua create mode 100644 target/tencent/board/generate_user_sig.lua create mode 100644 target/tencent/cos.lua create mode 100644 target/tencent/qywx.lua create mode 100644 target/tencent/wxofficial.lua create mode 100644 target/tencent/wxpay.lua create mode 100644 target/utils.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f14b74d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +################################################################################ +# 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。 +################################################################################ + +/.vs diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..44239d6 --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo 'INSTALL SUCCESS' \ No newline at end of file diff --git a/target/aliyun/ai.lua b/target/aliyun/ai.lua new file mode 100644 index 0000000..e5a0027 --- /dev/null +++ b/target/aliyun/ai.lua @@ -0,0 +1,36 @@ +local http = require("socket.http") +local ltn12 = require("ltn12") +local cjson = require("cjson") + +local M = {} +local url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions" + +M.ask = function(key,content,model) + -- 构造 JSON 格式的请求数据 + local payload = { + model = model, + messages = content + } + local response_body = {} + + local res, code, response_headers, status = http.request{ + url = url, + method = "POST", + headers = { + ["Authorization"] = "Bearer " .. key, + ["Content-Type"] = "application/json", + }, + source = ltn12.source.string(cjson.encode(payload)), + sink = ltn12.sink.table(response_body) + } + if code == 200 then + local data = cjson.decode(table.concat(response_body)) + return true,{content = data.choices[1].message.content,reasoning_content = data.choices[1].reasoning_content} + else + return false, "请求失败,错误描述:" .. table.concat(response_body) + end +end + +return M + + diff --git a/target/aliyun/email.lua b/target/aliyun/email.lua new file mode 100644 index 0000000..b81e278 --- /dev/null +++ b/target/aliyun/email.lua @@ -0,0 +1,191 @@ +local M = {} + +require("app.app") +local http = require("fwutils.httpclient") + + + +M.get_token = function(app_id,app_secret) + local token = cache.get_json("aliyun_email_token") + if token then + if token.expires > os.time() + 60*60 then + return true,token.access_token + end + end + + local url = "https://alimail-cn.aliyuncs.com/oauth2/v2.0/token" + local body = "grant_type=client_credentials&client_id="..app_id.."&client_secret="..app_secret + local result,err = http.post(url,{ + ["Content-Type"] = "application/x-www-form-urlencoded" + },body) + if not result then + return false,err + end + local data = cjson.decode(err) + if data.error ~= nil and data.error ~= "" then + return false,data.error_description + end + + cache.set_json("aliyun_email_token",{ + access_token = data.access_token, + expires = os.time() + data.expires_in + }) + return true,data.access_token +end + +M.create_msg = function(from,from_name,to,to_name,title,content,app_id,app_secret) + local ok,access_token = M.get_token(app_id,app_secret) + if not ok then + return false,access_token + end + local body = { + message = { + subject = title, + summary = content, + priority = "PRY_HIGH", + isReadReceiptRequested = false, + from = { + email = from, + name = from_name + }, + toRecipients = { + { + email = to, + name = to_name + } + }, + body = { + bodyText = content, + bodyHtml = content + } + } + } + local url = "https://alimail-cn.aliyuncs.com/v2/users/"..from.."/messages" + local result,err = http.post(url,{ + ["Content-Type"] = "application/json", + ["Authorization"] = "Bearer "..access_token + },cjson.encode(body)) + if not result then + return false,err + end + local data = cjson.decode(err) + if data == nil or data.message == nil or data.message.id == nil then + return false,err + end + return true,data.message.id +end +M.send = function(from,from_name,to,to_name,title,content,app_id,app_secret) + local ok,access_token = M.get_token(app_id,app_secret) + if not ok then + return false,access_token + end + local ok,msg_id = M.create_msg(from,from_name,to,to_name,title,content,app_id,app_secret) + if not ok then + return false,msg_id + end + local body = { + saveToSentItems = true + } + local url = "https://alimail-cn.aliyuncs.com/v2/users/"..from.."/messages/"..msg_id.."/send" + local result,err = http.post(url,{ + ["Content-Type"] = "application/json", + ["Authorization"] = "Bearer "..access_token + },cjson.encode(body)) + if not result then + return false,"send error:"..err + end + if err == "{}" then + return true + end + return false,"send error:"..err + --return true +end +M.create_user = function(email_name,password,nickname,jobtitle,app_id,app_secret) + local ok,access_token = M.get_token(app_id,app_secret) + if not ok then + return false,access_token + end + local body = { + email = email_name, + password = password, + name = nickname, + jobTitle = jobtitle, + departmentIds = {config.email.department_id}, + } + local url = "https://alimail-cn.aliyuncs.com/v2/users" + local result,err = http.post(url,{ + ["Content-Type"] = "application/json", + ["Authorization"] = "Bearer "..access_token + },cjson.encode(body)) + if not result then + return false,"send error:"..err + end + + local data = cjson.decode(err) + if data.email == email_name then + return true + end + return false,"create user error:"..err +end +M.delete_user = function(email_name,app_id,app_secret) + local ok,access_token = M.get_token(app_id,app_secret) + if not ok then + return false,access_token + end + local url = "https://alimail-cn.aliyuncs.com/v2/users/"..email_name + local result,err = http.delete(url,{ + ["Content-Type"] = "application/json", + ["Authorization"] = "Bearer "..access_token + }) + if not result then + return false,"send error:"..err + end + return true +end +M.getDepartment = function(id,app_id,app_secret) + local ok,access_token = M.get_token(app_id,app_secret) + if not ok then + return false,access_token + end + + + local url = "https://alimail-cn.aliyuncs.com/v2/departments/"..id + local result,err = http.get(url,{ + ["Content-Type"] = "application/json", + ["Authorization"] = "Bearer "..access_token + }) + + if not result then + return false,"send error:"..err + end + print("getDepartment") + print("result",result) + print("err",type(err)) + print("err[data]",err) + + return true +end +M.getDepartmentList = function(app_id,app_secret) + local ok,access_token = M.get_token(app_id,app_secret) + if not ok then + return false,access_token + end + + local url = "https://alimail-cn.aliyuncs.com/v2/departments/$root/chain" + local result,err = http.get(url,{ + ["Content-Type"] = "application/json", + ["Authorization"] = "Bearer "..access_token + }) + if not result then + return false,"send error:"..err + end + print("getDepartmentList") + print("result",result) + print("err",type(err)) + print("err[data]",err) + + return true +end + +return M + diff --git a/target/base64.lua b/target/base64.lua new file mode 100644 index 0000000..af7dd2c --- /dev/null +++ b/target/base64.lua @@ -0,0 +1,52 @@ +local M = {} + +-- Base64 字符集 +local b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' +-- 创建 Base64 反向查找表 +local b64lookup = {} +for i = 1, #b64chars do + b64lookup[string.sub(b64chars, i, i)] = i - 1 +end + +M.encode = function(input) + local output = {} + local len = #input + for i = 1, len, 3 do + local a, b, c = string.byte(input, i, i + 2) + local chunk = (a or 0) << 16 | (b or 0) << 8 | (c or 0) + + output[#output + 1] = b64chars:sub(((chunk >> 18) & 0x3F) + 1, ((chunk >> 18) & 0x3F) + 1) + output[#output + 1] = b64chars:sub(((chunk >> 12) & 0x3F) + 1, ((chunk >> 12) & 0x3F) + 1) + output[#output + 1] = b and b64chars:sub(((chunk >> 6) & 0x3F) + 1, ((chunk >> 6) & 0x3F) + 1) or "=" + output[#output + 1] = c and b64chars:sub((chunk & 0x3F) + 1, (chunk & 0x3F) + 1) or "=" + end + return table.concat(output) +end + +-- Base64 解码函数 +M.decode = function(input) + input = input:gsub("%s", ""):gsub("=", "") -- 去除空白和填充符 + local output = {} + for i = 1, #input, 4 do + local a, b, c, d = b64lookup[input:sub(i, i)], b64lookup[input:sub(i + 1, i + 1)], + b64lookup[input:sub(i + 2, i + 2)] or 0, b64lookup[input:sub(i + 3, i + 3)] or 0 + local chunk = (a << 18) | (b << 12) | (c << 6) | d + + output[#output + 1] = string.char((chunk >> 16) & 0xFF) + if input:sub(i + 2, i + 2) ~= "" then + output[#output + 1] = string.char((chunk >> 8) & 0xFF) + end + if input:sub(i + 3, i + 3) ~= "" then + output[#output + 1] = string.char(chunk & 0xFF) + end + end + return table.concat(output) +end + + + + + +return M + + diff --git a/target/cache.lua b/target/cache.lua new file mode 100644 index 0000000..802ac6b --- /dev/null +++ b/target/cache.lua @@ -0,0 +1,40 @@ +local M = {} + +local localstorage = require("localstorage") +local fw = require("fastweb") +local cjson = require("cjson") + +M.start = function(dirpath) + local storage = localstorage.new() + fw.set_ptr("localstorage_cache",storage:self()) + if storage:open(dirpath) == false then + return false,storage:last_error() + end + return true +end + +M.close = function() + localstorage.new(localstorage_cache):close() +end +M.get = function(key) + return localstorage.new(localstorage_cache):read(key) +end +M.get_json = function(key) + local value = M.get(key) + if value then + return cjson.decode(value) + end + return nil +end +M.set = function(key,value) + localstorage.new(localstorage_cache):write(key,value) +end +M.set_json = function(key,value) + M.set(key,cjson.encode(value)) +end +M.del = function(key) + localstorage.new(localstorage_cache):del(key) +end +return M + + diff --git a/target/fwutils/acl.lua b/target/fwutils/acl.lua new file mode 100644 index 0000000..92c8fcf --- /dev/null +++ b/target/fwutils/acl.lua @@ -0,0 +1,117 @@ +local utils = require("utils") +local fw = require("fastweb") +local config = require("fwutils.config") +local M = {} + + +-- 更新 +M.update = function(role_id,conn) + -- 查询权限表 + local select = conn:select() + select:table("fw_role_permissions") + select:where_expression("AND delete_time IS NULL") + if role_id ~= nil then + select:where_i32("role_id", "=", role_id) + end + local result = select:query() + + + local bc = {} + + while result:next() do + local id = result:get("id") + local path = result:get("path") + local role_id = tostring(result:get("role_id")) + local action = result:get("action") + local desc = result:get("desc") + local create_time = result:get("create_time") + local update_time = result:get("update_time") + local delete_time = result:get("delete_time") + -- local public = result:get("public") + + if bc[role_id] == nil then + bc[role_id] = {} + end + if bc[role_id]["public"] == nil then + bc[role_id]["public"] = {} + end + if bc[role_id]["private"] == nil then + bc[role_id]["private"] = {} + end + -- 处理 action 字段,将其切分为表或空表 + local actions_tbl = {} + if action and action ~= "" then + for act in string.gmatch(action, "([^,]+)") do + table.insert(actions_tbl, act) + end + end + + local item = { + create_time = create_time, + update_time = update_time, + delete_time = delete_time, + action = actions_tbl, + desc = desc, + } + -- if public == 1 then + -- bc[role_id]["public"][path] = item + -- else + -- bc[role_id]["private"][path] = item + -- end + bc[role_id][path] = item + end + local code = "return " .. require("serpent").serialize(bc, {comment = false}) + utils.save_file(fw.website_dir().."/"..(config.path.luabytecode:gsub("%.", "/")).."/acl_bc.lua",code) + return true +end +-- 匹配 +M.match = function(cfg) + + local function match_path(path, patterns) + + -- print("[match_path] path:",path) + for pattern, v in pairs(patterns) do + + -- 如果是正则(以^开头),用string.match,否则精确匹配 + if string.sub(pattern, 1, 1) == "^" then + + if string.match(path, pattern) then + -- print("[TRUE] pattern:",pattern,",path:",path) + return true, v + -- else + -- print("[FALSE] pattern:",pattern,",path:",path) + end + else + if path == pattern then + return true, v + end + end + end + return false, nil + end + -- 检查action + local function check_action(actions,action) + if actions == nil or #actions == 0 then + return true + end + + for _,v in pairs(actions) do + if v == action then + return true + end + end + return false, "action not match" + end + local role_id_str = string.format("%d",cfg.role_id()) + local acl_bc = require(config.path.luabytecode..".acl_bc") + if acl_bc[role_id_str] == nil then + return false,"role id("..role_id_str..") acl not found" + end + local result, item = match_path(cfg.filepath(), acl_bc[role_id_str]) + if result then + return check_action(item.action,cfg.action()) + end + return false,"path("..cfg.filepath()..") acl not found" +end + +return M \ No newline at end of file diff --git a/target/fwutils/config.lua b/target/fwutils/config.lua new file mode 100644 index 0000000..2d3112b --- /dev/null +++ b/target/fwutils/config.lua @@ -0,0 +1,22 @@ +return { + -- 服务器ID,如果REDIS或MYSQL与其它服务公用必须更改此参数,要求唯一 + server_id = "TEACHER_1234567890", + + path = { + luabytecode = "cache.fw.luabytecode" + }, + token = { + -- 超时时间 + expire = 3600 * 24 * 30, + -- 加密算法 + algorithm = "aes-256", + -- 加密模式 + mode = "cbc", + -- 加密密钥 + key = "kangDzFLc3MweDQH", + }, + db = { + redis_pool_name = "rdb", + mysql_pool_name = "db", + } +} \ No newline at end of file diff --git a/target/fwutils/init.lua b/target/fwutils/init.lua new file mode 100644 index 0000000..279ac3f --- /dev/null +++ b/target/fwutils/init.lua @@ -0,0 +1,41 @@ +local mysql_pool = require("mysql.pool") +local fw = require("fastweb") +local utils = require("utils") + + +local M = {} + + +M.initialization = function(conn) + + local result,err = M.__get_guest_role_id(conn) + if result == false then + return false,err + end + + return true +end +M.__get_guest_role_id = function(mysql_conn) + local ppst = mysql_conn:setsql("SELECT * FROM fw_role WHERE guest = 1") + local result = ppst:query() + if result:row_count() ~= 1 then + return false,"role guest not found" + end + + result:next() + local bc = { + GUEST_ROLE_ID = result:get("id"), + } + local config = require("fwutils.config") + local code = "return " .. require("serpent").serialize(bc, {comment = false}) + utils.save_file(fw.website_dir().."/"..(config.path.luabytecode:gsub("%.", "/")).."/fwcache.lua",code) + return true +end +M.guest_role_id = function() + local config = require("fwutils.config") + return require(config.path.luabytecode..".fwcache").GUEST_ROLE_ID +end + + + +return M \ No newline at end of file diff --git a/target/fwutils/menu.lua b/target/fwutils/menu.lua new file mode 100644 index 0000000..ddae635 --- /dev/null +++ b/target/fwutils/menu.lua @@ -0,0 +1,104 @@ +local fw = require("fastweb") +local config = require("fwutils.config") +local M = {} +-- CREATE TABLE `fw_menu` ( +-- `id` int NOT NULL AUTO_INCREMENT, +-- `role_id` int DEFAULT NULL, +-- `title` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, +-- `path` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, +-- `icon` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, +-- `sort` int DEFAULT NULL, +-- `parent_id` int DEFAULT NULL, +-- PRIMARY KEY (`id`) +-- ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 更新 +M.update = function(role_id,conn) + -- 查询菜单表 + local select = conn:select() + select:table("fw_menu") + select:where_expression("AND delete_time IS NULL") + if role_id ~= nil then + select:where_i32("role_id", "=", role_id) + end + local result = select:query() + + local bc = {} + local items_by_id = {} -- 通过id快速查找菜单项:{id = {role_id, title, item, parent_id}} + local items_with_parent = {} -- 存储有父级的菜单项 + + -- 第一遍:读取所有菜单项并存储 + while result:next() do + local id = result:get("id") + local role_id = tostring(result:get("role_id")) + local title = result:get("title") + local path = result:get("path") + local icon = result:get("icon") + local sort = result:get("sort") + local parent_id = result:get("parent_id") + + if not title or title == "" then + goto continue + end + + if bc[role_id] == nil then + bc[role_id] = {} + end + + local item = { + path = path, + icon = icon, + sort = sort, + } + + -- 存储所有菜单项信息 + items_by_id[id] = { + role_id = role_id, + title = title, + item = item, + parent_id = parent_id + } + + -- 如果parent_id为空,则作为顶层菜单项 + if parent_id == nil or parent_id == 0 then + bc[role_id][title] = item + else + -- 有父级,记录下来稍后处理 + table.insert(items_with_parent, { + id = id, + role_id = role_id, + title = title, + item = item, + parent_id = parent_id + }) + end + + ::continue:: + end + + -- 第二遍:处理有父级的菜单项,构建children结构 + for _, menu_item in ipairs(items_with_parent) do + local parent_info = items_by_id[menu_item.parent_id] + if parent_info then + local parent_item = parent_info.item + + -- 如果父项还没有children表,创建它 + if not parent_item.children then + parent_item.children = {} + end + + -- 将子项添加到父项的children中 + parent_item.children[menu_item.title] = menu_item.item + end + end + + local code = "return " .. require("serpent").serialize(bc, {comment = false}) + utils.save_file(fw.website_dir().."/"..(config.path.luabytecode:gsub("%.", "/")).."/menu_bc.lua",code) + return true +end +M.get = function(role_id) + local menu_bc = require(config.path.luabytecode..".menu_bc") + return menu_bc[string.format("%d",role_id)] +end + +return M \ No newline at end of file diff --git a/target/fwutils/request_config.lua b/target/fwutils/request_config.lua new file mode 100644 index 0000000..369c5f6 --- /dev/null +++ b/target/fwutils/request_config.lua @@ -0,0 +1,64 @@ +local request = require("fastweb.request") +local token_module = require("fwutils.token") +local config = require("fwutils.config") +local cjson = require("cjson") +local utils = require("utils") +local fwutils_init = require("fwutils.init") +local M = {} +M.__filepath = nil +M.__method = nil +M.__url_param = nil +M.__action = nil +M.__user_data = nil +M.__role_id = nil +M.__ext = nil + +M.init = function(website_config) + M.__filepath = request.filepath() + M.__method = request.method() + M.__url_param = request.url_param() + M.__action = nil + M.__user_data = nil + M.__role_id = nil + M.__ext = utils.ext(M.__filepath) + -- 修饰路径 + if M.__filepath == "/" then + M.__filepath = website_config.default_filepath + end + -- 获取动作 + if M.__url_param ~= nil then + M.__action = M.__url_param["action"] + end + -- 获取TOKEN + local token = string.match(request.header("Cookie"),"token=(%w+)") + if token ~= nil and token ~= "" then + local result,user_data = token_module.get(token) + -- print("TOKEN_DATA:",cjson.encode(user_data)) + if result and user_data ~= nil and user_data["role_id"] ~= nil and user_data["role_id"] > 0 then + M.__user_data = user_data + M.__role_id = user_data["role_id"] + request.set("user_data",cjson.encode(user_data)) + else + M.__role_id = fwutils_init.guest_role_id() + end + else + -- 访客 + M.__role_id = fwutils_init.guest_role_id() + end +end +M.role_id = function() + return M.__role_id +end +M.user_data = function() + return M.__user_data +end +M.action = function() + return M.__action +end +M.filepath = function() + return M.__filepath +end +M.ext = function() + return M.__ext +end +return M \ No newline at end of file diff --git a/target/fwutils/stopwatch.lua b/target/fwutils/stopwatch.lua new file mode 100644 index 0000000..1a1ff50 --- /dev/null +++ b/target/fwutils/stopwatch.lua @@ -0,0 +1,34 @@ +local M = {} + +M.tss = {} + +M.record = function(name) + if name == nil then + name = "" + end + table.insert(M.tss, { + name = name, + ts = fw_now_msec(), + }) +end + +M.print = function() + + for i = 2, #M.tss do + local prev = M.tss[i - 1] + local curr = M.tss[i] + local diff = curr.ts - prev.ts + if diff < 1000 then + print(string.format("%s --> %dms", curr.name, diff)) + else + print(string.format("%s --> %.2fs", curr.name, diff / 1000)) + end + end + M.tss = {} + +end + + + + +return M \ No newline at end of file diff --git a/target/fwutils/template_engine.lua b/target/fwutils/template_engine.lua new file mode 100644 index 0000000..0e0e37e --- /dev/null +++ b/target/fwutils/template_engine.lua @@ -0,0 +1,435 @@ +local utils = require("utils") +local fw = require("fastweb") +local config = require("fwutils.config") +local request = require("fastweb.request") +local response = require("fastweb.response") +local cjson = require("cjson") +-- 允许的扩展名 +local allowed_extensions = { + "shtml", + "html", + "js" +} +local template_engine_bc_this_role = nil +local cfg = nil + +local M = {} + +function file_get_contents(filepath) + + local file, errmsg = io.open(fw.website_dir()..filepath, "r") + if not file then + err.server(errmsg) + end + local content = file:read("*a") + if content == nil or content == "" then + print("file_get_contents error: ",filepath) + return "" + end + local replaced,c2 = M.replace(content) + if replaced then + return c2 + end + return content +end +function menu_top() + -- 当前请求路径 + local request_path = request.filepath() or "" + -- 取配置 + local menuData = require("fwutils.menu").get(string.format("%d",cfg.role_id())) + if not menuData then + return "" + 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 = [[ + + + ]] + + return [=[ + + ]] +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 = "
" + for i,v in ipairs(photos) do + if type(v) == "string" then + content = content..[[
+
+ +
+
+ ]] + 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 \ No newline at end of file diff --git a/target/fwutils/token.lua b/target/fwutils/token.lua new file mode 100644 index 0000000..1ab3f75 --- /dev/null +++ b/target/fwutils/token.lua @@ -0,0 +1,92 @@ +local config = require("fwutils.config") +local codec = require("fastweb.codec") +local utils = require("utils") +local cjson = require("cjson") +local redis_pool = require("redis.pool") +local M = {} +local function make_key(id,token) + if type(id) == "number" then + id = string.format("%d",id) + end + return config.server_id.."_token_"..id.."_"..token +end +M.__parse_token = function(token) + if token == nil or token == "" then + return false,"token is required" + end + local token_data = codec.aes_de(config.token.key,utils.hex_to_bytes(token),config.token.algorithm,config.token.mode) + if token_data == nil or token_data == "" then + return false,"token is invalid(a)" + end + local token_data_json = cjson.decode(utils.hex_to_bytes(token_data)) + if token_data_json == nil or token_data_json.id == nil or token_data_json.id <= 0 then + return false,"token is invalid(no id)" + end + return true,token_data_json +end +M.get = function(token) + -- 解析TOKEN + local result,token_data_json = M.__parse_token(token) + if not result then + return false,token_data_json + end + + + -- 获取TOKEN数据 + local conn = redis_pool.new(_G[config.db.redis_pool_name]):get() + + local data = conn:get(make_key(token_data_json["id"],token)) + if data ~= nil and data ~= "" then + return true,cjson.decode(data) + end + return false,"token is invalid" +end +M.set = function(token,data) + -- 解析TOKEN + local result,token_data_json = M.__parse_token(token) + if not result then + return false,token_data_json + end + if data == nil then + return false,"data is required" + end + local conn = redis_pool.new(_G[config.db.redis_pool_name]):get() + conn:setex(make_key(token_data_json["id"],token),config.token.expire,cjson.encode(data)) + return true +end +M.del_by_id = function(id) + if id == nil or id <= 0 then + return false,"id is required" + end + local conn = redis_pool.new(_G[config.db.redis_pool_name]):get() + local keys = conn:keys(config.other.SERVER_ID.."_token_"..string.format("%d",id).."_*") + for _,key in ipairs(keys) do + conn:del(key) + end + return true +end +M.del_by_token = function(token) + -- 解析TOKEN + local result,token_data_json = M.__parse_token(token) + if not result then + return false,token_data_json + end + local conn = redis_pool.new(_G[config.db.redis_pool_name]):get() + conn:del(make_key(token_data_json["id"],token)) + return true +end +M.create = function(id,data) + if id == nil or id <= 0 then + return false,"id is required" + end + local key_data = { + id = id, + create_time = fw.now_msec() + } + local token = codec.aes_en(config.token.key,cjson.encode(key_data),config.token.algorithm,config.token.mode) + if M.set(token,data) == false then + return false,"create token failed" + end + return true,token +end +return M \ No newline at end of file diff --git a/target/httpclient.lua b/target/httpclient.lua new file mode 100644 index 0000000..fb8b7b0 --- /dev/null +++ b/target/httpclient.lua @@ -0,0 +1,71 @@ +local http = require("socket.http") +local ltn12 = require("ltn12") +local cjson = require("cjson") +require("app.app") + +local M = {} + +M.get = function(url,headers) + local response_body = {} + local res, status_code, response_headers, status_text = http.request{ + url = url, + method = "GET", + headers = headers, + sink = ltn12.sink.table(response_body) + } + + if not res then + return false,"err_http_get1,status_code:"..(status_code or "N/A")..",status_text:"..(status_text or "N/A")..",response_body:"..table.concat(response_body) + end + if status_code ~= 200 then + return false,"err_http_get2,status_code:"..(status_code or "N/A")..",status_text:"..(status_text or "N/A")..",response_body:"..table.concat(response_body) + end + return true,table.concat(response_body) +end + +M.post = function(url,headers,body) + local response_body = {} + headers["Content-Length"] = tostring(#body) + local res, status_code, response_headers, status_text = http.request{ + url = url, + method = "POST", + headers = headers, + source = ltn12.source.string(body), + sink = ltn12.sink.table(response_body) + } + if not res then + return false,"err_http_post,status_code:"..(status_code or "N/A")..",status_text:"..(status_text or "N/A")..",response_body:"..table.concat(response_body) + end + if status_code ~= 200 then + return false,"err_http_post,status_code:"..(status_code or "N/A")..",status_text:"..(status_text or "N/A")..",response_body:"..table.concat(response_body) + end + return true,table.concat(response_body) +end +M.delete = function(url,headers) + local response_body = {} + local res, status_code, response_headers, status_text = http.request{ + url = url, + method = "DELETE", + headers = headers, + sink = ltn12.sink.table(response_body) + } + + if not res then + return false,"err_http_delete,status_code:"..(status_code or "N/A")..",status_text:"..(status_text or "N/A")..",response_body:"..table.concat(response_body) + end + if status_code ~= 200 then + return false,"err_http_delete,status_code:"..(status_code or "N/A")..",status_text:"..(status_text or "N/A")..",response_body:"..table.concat(response_body) + end + return true,table.concat(response_body) +end +-- 生成 A=B&C=D 的格式 +M.build_query = function(params) + local query = {} + for k, v in pairs(params) do + table.insert(query, k .. "=" .. v) + end + return table.concat(query, "&") +end + + +return M \ No newline at end of file diff --git a/target/submail/sms.lua b/target/submail/sms.lua new file mode 100644 index 0000000..557d0eb --- /dev/null +++ b/target/submail/sms.lua @@ -0,0 +1,56 @@ +local M = {} +local httpclient = require("fwutils.httpclient") +local sms_url = "https://api-v4.mysubmail.com/sms/xsend" +local sms_world_url = "https://api-v4.mysubmail.com/internationalsms/xsend" +M.send = function(phone,appid,appkey,signature,template_code,vars) + + local data = { + appid = appid, + signature = appkey, + to = phone, + project = template_code, + vars = cjson.encode(vars), + sms_signature = signature, + } + local result,msg = httpclient.post(sms_url,{ + ["Content-Type"] = "application/json", + },cjson.encode(data)) + + if result then + local json = cjson.decode(msg) + if json.status == "success" then + return true + else + return false,json.msg + end + else + return false,msg + end +end +M.send_world = function(phone,appid,appkey,template_code,vars) + + local data = { + appid = appid, + signature = appkey, + to = phone, + project = template_code, + vars = cjson.encode(vars) + + } + print(cjson.encode(data)) + local result,msg = httpclient.post(sms_world_url,{ + ["Content-Type"] = "application/json", + },cjson.encode(data)) + + if result then + local json = cjson.decode(msg) + if json.status == "success" then + return true + else + return false,json.msg + end + else + return false,msg + end +end +return M \ No newline at end of file diff --git a/target/tencent/board.lua b/target/tencent/board.lua new file mode 100644 index 0000000..b153599 --- /dev/null +++ b/target/tencent/board.lua @@ -0,0 +1,7 @@ +require("app.app") +local M = {} + + + + +return M \ No newline at end of file diff --git a/target/tencent/board/generate_user_sig.lua b/target/tencent/board/generate_user_sig.lua new file mode 100644 index 0000000..4ac1af9 --- /dev/null +++ b/target/tencent/board/generate_user_sig.lua @@ -0,0 +1,77 @@ +--[[ +* Module: GenerateUserSig (Lua version) +* Function: Generate UserSig for Tencent Cloud IM/Board SDK +* Reference: C++ implementation provided +* Note: This implementation uses Lua's standard libraries and assumes the presence of 'openssl' and 'zlib' Lua modules. +* Usage: require this module and call gen_user_sig(user_id, sdkappid, secretkey, expiretime) +--]] +require("app.app") +local zlib = require("zlib") +local base64 = require("app.module.base64") +local M = {} + +-- Helper: base64 encode, then replace +, /, = as required by Tencent +local function base64_url_encode(data) + local b64 = base64.encode(data) + -- 按照C++代码的字符替换规则 + b64 = b64:gsub("+", "*"):gsub("/", "-"):gsub("=", "_") + return b64 +end + +-- Helper: HMAC-SHA256 +local function hmac_sha256(key, msg) + -- 确保参数都是字符串类型 + return codec.hmac_sha256(tostring(key), tostring(msg)) +end + +-- Helper: hex string -> binary string +local function hex_to_bin(hex) + return (hex:gsub("..", function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +-- Helper: Generate the HMAC-SHA256 signature string +local function gen_hmac_sig(user_id, sdkappid, curr_time, expire_time, secret_key) + local content = string.format( + "TLS.identifier:%s\nTLS.sdkappid:%d\nTLS.time:%d\nTLS.expire:%d\n", + user_id, sdkappid, curr_time, expire_time + ) + -- 底层返回十六进制摘要,需转为二进制再做标准Base64 + local sig_hex = hmac_sha256(secret_key, content) + local sig_bin = hex_to_bin(sig_hex) + return base64.encode(sig_bin) +end + +-- Main: Generate the UserSig JSON, compress, and base64 encode +local function gen_user_sig(user_id, sdkappid, secretkey, expiretime) + assert(user_id and user_id ~= "", "user_id must not be empty") + assert(tonumber(sdkappid) and tonumber(sdkappid) > 0, "sdkappid must be a positive integer") + assert(tonumber(expiretime) and tonumber(expiretime) > 0, "expiretime must be a positive integer") + assert(secretkey and secretkey ~= "", "secretkey must not be empty") + + local curr_time = os.time() + -- 按官方JS示例:TLS.expire 使用持续时间(秒),不是绝对时间戳 + local expire_time = expiretime + + local sig = gen_hmac_sig(user_id, sdkappid, curr_time, expire_time, secretkey) + + -- Compose JSON string (严格按照C++代码的格式和顺序) + local json = string.format( + '{"TLS.ver":"2.0","TLS.identifier":"%s","TLS.sdkappid":%d,"TLS.expire":%d,"TLS.time":%d,"TLS.sig":"%s"}', + user_id, sdkappid, expire_time, curr_time, sig + ) + + -- Compress with zlib (raw deflate) - 使用Z_BEST_SPEED压缩级别 + local deflater = zlib.deflate() + local compressed, eof, bytes_in, bytes_out = deflater(json, "finish") + + -- Base64 encode and replace chars as required + local user_sig = base64_url_encode(compressed) + return user_sig +end + +-- Public API +M.gen_user_sig = gen_user_sig + +return M diff --git a/target/tencent/cos.lua b/target/tencent/cos.lua new file mode 100644 index 0000000..2deb0e2 --- /dev/null +++ b/target/tencent/cos.lua @@ -0,0 +1,150 @@ +local tencent_cos = require("tencent_cos") +local fw = require("fastweb") +local utils = require("app.utils") +local config = require("app.config") +local M = {} +-- 文件扩展名与目录的映射 +M.ext_dirpath = { + images = { + "jpg", + "jpeg", + "png", + "gif", + "bmp", + "webp", + "svg", + "ico", + "tif", + "tiff", + }, + videos = { + "mp4", + "avi", + "mov", + "wmv", + "flv" + }, + audio = { + "mp3", + "wav", + "ogg", + "aac", + "m4a", + "wma" + }, + documents = { + "pdf", + "doc", + "docx", + "xls", + "xlsx", + "ppt", + "pptx" + } +} + +function M.upload_data(config,object_name,remote_dir,data,ext) + + local filename = fw.make_software_guid().."."..ext + local dirpath = "" + + + for dir, ext_list in pairs(M.ext_dirpath) do + for _, ext_type_ext in ipairs(ext_list) do + if ext_type_ext == ext then + dirpath = dir + break + end + end + if dirpath ~= "" then + break + end + end + if dirpath == "" then + return false,"不支持的文件格式,ext: "..ext + end + + if remote_dir == nil or remote_dir == "" then + remote_dir = "" + else + remote_dir = remote_dir.."/" + end + + + local cos = tencent_cos.new() + + + local local_file = fw.website_dir()..config.path.temp.."/"..filename + utils.save_file(local_file,data) + + local cos_filepath = remote_dir..dirpath.."/"..filename + local result = cos:upfile( + config.appid, + config.endpoint, + config.secret_id, + config.secret_key, + object_name, + cos_filepath, + local_file + ) + if result == "" then + return true,cos_filepath + end + return false,result +end +function M.upload_file(config,object_name,remote_dir,local_filepath,auto_remove) + + if not utils.exists_file(local_filepath) then + return false,"文件不存在,local_filepath: "..local_filepath + end + local ext = utils.ext(local_filepath) + local filename = fw.make_software_guid().."."..ext + + local dirpath = "" + + for dir, ext_list in pairs(M.ext_dirpath) do + for _, ext_type_ext in ipairs(ext_list) do + if ext_type_ext == ext then + dirpath = dir + break + end + end + if dirpath ~= "" then + break + end + end + if dirpath == "" then + return false,"不支持的文件格式,ext: "..ext + end + + if remote_dir == nil or remote_dir == "" then + remote_dir = "" + else + remote_dir = remote_dir.."/" + end + + local cos_filepath = remote_dir .. dirpath.."/"..filename + local cos = tencent_cos.new() + + + local result = cos:upfile( + config.appid, + config.endpoint, + config.secret_id, + config.secret_key, + object_name, + cos_filepath, + local_filepath + ) + if result == "" then + if auto_remove then + utils.delete_file(local_filepath) + end + return true,cos_filepath + end + return false,result +end + + + +return M \ No newline at end of file diff --git a/target/tencent/qywx.lua b/target/tencent/qywx.lua new file mode 100644 index 0000000..1a834d0 --- /dev/null +++ b/target/tencent/qywx.lua @@ -0,0 +1,286 @@ +local http = require("fwutils.httpclient") +local codec = require("fastweb.codec") +local base64 = require("fwutils.base64") +local openssl = require("openssl") +local M = {} + +local url_get_access_token = "https://qyapi.weixin.qq.com/cgi-bin/gettoken" +local url_send_message = "https://qyapi.weixin.qq.com/cgi-bin/message/send" +local url_get_userid_by_mobile = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserid" +local url_get_userids = "https://qyapi.weixin.qq.com/cgi-bin/user/list_id" + + +-- 验证签名 +M.verify_signature = function(token,timestamp,nonce,echostr) + local params = {token, timestamp, nonce, echostr} + table.sort(params, function(a, b) return tostring(a) < tostring(b) end) + return string.lower(codec.sha1(table.concat(params))) +end + +-- 解密数据 +M.decrypt_data = function(qywx_aeskey,data) + + -- 16随机数+4字节消息长度+消息体+接收ID + local function parse_decrypted_msg(data) + -- data: string, decrypted_echostr + if #data < 20 then + return nil, "data too short" + end + -- 跳过16字节随机数 + local msg_len_bytes = data:sub(17, 20) + local b1, b2, b3, b4 = msg_len_bytes:byte(1, 4) + local msg_len = b1 * 2^24 + b2 * 2^16 + b3 * 2^8 + b4 + local msg_start = 21 + local msg_end = 20 + msg_len + local msg = data:sub(msg_start, msg_end) + return msg + end + + local cipher = openssl.cipher.new("aes-256-cbc") + cipher:init(base64.decode(qywx_aeskey), "0123456789abcdef", false) + local plaintext = cipher:update(data) + plaintext = parse_decrypted_msg(plaintext) + return plaintext +end + + +-- 获取access_token +M.get_access_token = function(corpid,corpsecret) + + local url = url_get_access_token .. "?corpid=" .. corpid .. "&corpsecret=" .. corpsecret + local result,err = http.get(url,{ + ["Content-Type"] = "application/json" + }) + if not result then + return false,err + end + local data = cjson.decode(err) + if data.errcode == 0 then + return true,data.access_token + end + return false,data.errmsg +end +-- 获取userid +M.get_userid_by_mobile = function(access_token,mobile) + local url = url_get_userid_by_mobile .. "?access_token=" .. access_token .. "&mobile=" .. mobile + local result,err = http.post(url,{ + ["Content-Type"] = "application/json" + },cjson.encode({ + mobile = mobile + })) + if not result then + return false,err + end + local data = cjson.decode(err) + if data.errcode == 0 then + return true,data.userid + end + return false,data.errmsg +end +-- 获取用户列表 +M.get_userids = function(access_token) + local url = url_get_userids .. "?access_token=" .. access_token + local result,err = http.post(url,{ + ["Content-Type"] = "application/json" + },cjson.encode({ + limit = 1000 + })) + if not result then + return false,err + end + local data = cjson.decode(err) + if data.errcode == 0 then + return true,data.dept_user + end + return false,data.errmsg +end +-- 上传临时文件 +M.upload_temp_file = function(access_token,data,type_str) + local function make_upload_temp_data(data) + local boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW" + local body = "" + + local filename = "" + if type_str == "image" then + filename = tostring(os.time())..".png" + elseif type_str == "video" then + filename = tostring(os.time())..".mp4" + end + + -- 构造 multipart 数据 + body = body .. "--" .. boundary .. "\r\n" + body = body .. "Content-Disposition: form-data; name=\"media\"; filename=\""..filename.."\"\r\n" + body = body .. "Content-Type: application/octet-stream\r\n\r\n" .. data .. "\r\n" + body = body .. "--" .. boundary .. "--\r\n" + + return body + end + local body = make_upload_temp_data(data) + local url = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=" .. access_token.."&type="..type_str + local result,err = http.post(url,{ + ["Content-Type"] = "multipart/form-data; boundary=--WebKitFormBoundary7MA4YWxkTrZu0gW", + ["Content-Length"] = #body + },body) + if not result then + return false,err + end + local data = cjson.decode(err) + if data.errcode == 0 then + return true,data.media_id + end + return false,data.errmsg +end +-- 发送消息 +M.send_message = function(access_token,userid,agentid,message) + local url = url_send_message .. "?access_token=" .. access_token + local result,err = http.post(url,{ + ["Content-Type"] = "application/json" + },cjson.encode({ + touser = userid, + msgtype = "text", + agentid = agentid, + text = { + content = message + } + })) + if not result then + return false,err + end + local data = cjson.decode(err) + if data.errcode == 0 then + return true + end + return false,data.errmsg +end +-- 发送图片消息 +M.send_image_message = function(access_token,userid,agentid,image_filepath) + local file = io.open(image_filepath, "rb") + if not file then + return false, "cannot open file: " .. tostring(image_filepath) + end + local data = file:read("*a") + file:close() + local ok, media_id = M.upload_temp_file(access_token, data,"image") + if not ok then + return false, "upload_temp_file_failed:"..media_id + end + local url = url_send_message .. "?access_token=" .. access_token + local result, err = http.post(url, { + ["Content-Type"] = "application/json" + }, cjson.encode({ + touser = userid, + msgtype = "image", + agentid = agentid, + image = { + media_id = media_id, + } + })) + if not result then + return false, err + end + local resp = cjson.decode(err) + if resp.errcode == 0 then + return true + end + return false, resp.errmsg +end +-- 发送视频消息 +M.send_video_message = function(access_token,userid,agentid,title,video_filepath) + local file = io.open(video_filepath, "rb") + if not file then + return false, "cannot open file: " .. tostring(video_filepath) + end + local data = file:read("*a") + file:close() + local ok, media_id = M.upload_temp_file(access_token, data,"video") + if not ok then + return false, "upload_temp_file_failed:"..media_id + end + local url = url_send_message .. "?access_token=" .. access_token + local result, err = http.post(url, { + ["Content-Type"] = "application/json" + }, cjson.encode({ + touser = userid, + msgtype = "video", + agentid = agentid, + video = { + media_id = media_id, + title = title + } + })) + if not result then + return false, err + end + local resp = cjson.decode(err) + if resp.errcode == 0 then + return true + end + return false, resp.errmsg +end +-- 发送卡片消息 +M.send_card_message = function(access_token,userid,agentid,message) + local url = url_send_message .. "?access_token=" .. access_token + local result,err = http.post(url,{ + ["Content-Type"] = "application/json" + },cjson.encode({ + touser = userid, + msgtype = "news", + agentid = agentid, + news = { + articles = { + message + } + } + })) + if not result then + return false,err + end + local data = cjson.decode(err) + if data.errcode == 0 then + return true + end + return false,data.errmsg +end +-- 发送卡片消息 +M.send_template_card_message = function(access_token,userid,agentid,message) + local url = url_send_message .. "?access_token=" .. access_token + local result,err = http.post(url,{ + ["Content-Type"] = "application/json" + },cjson.encode({ + touser = userid, + msgtype = "template_card", + agentid = agentid, + template_card = message + })) + if not result then + return false,err + end + local data = cjson.decode(err) + if data.errcode == 0 then + return true + end + return false,data.errmsg +end +-- 发送MMD消息 +M.send_markdown_message = function(access_token,userid,agentid,message) + local url = url_send_message .. "?access_token=" .. access_token + local result,err = http.post(url,{ + ["Content-Type"] = "application/json" + },cjson.encode({ + touser = userid, + msgtype = "markdown", + agentid = agentid, + markdown = { + content = message + } + })) + if not result then + return false,err + end + local data = cjson.decode(err) + if data.errcode == 0 then + return true + end + return false,data.errmsg +end +return M \ No newline at end of file diff --git a/target/tencent/wxofficial.lua b/target/tencent/wxofficial.lua new file mode 100644 index 0000000..0e1a71d --- /dev/null +++ b/target/tencent/wxofficial.lua @@ -0,0 +1,218 @@ +require("app.app") +local http = require("socket.http") +local ltn12 = require("ltn12") +local httpclient = require("fwutils.httpclient") +local cache = require("fwutils.cache") + + +local M = {} + +-- access_token 的 url +M.url_get_access_token = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential" + +-- 获取用户access_token的url +M.url_get_user_access_token = "https://api.weixin.qq.com/sns/oauth2/access_token?grant_type=authorization_code" + +-- 获取用户信息的url +M.url_get_user_info = "https://api.weixin.qq.com/sns/userinfo?lang=zh_CN" +-- 授权地址 +M.url_auth = "https://open.weixin.qq.com/connect/oauth2/authorize" +-- 模板消息 +M.url_template_message = "https://api.weixin.qq.com/cgi-bin/message/template/send" +-- 获取 access_token +M.get_access_token = function(appid, appsecret) + -- local value = cache.get_json("wxofficial_info") + -- if value then + -- -- 是否距离过期至少还有5分钟 + -- if value.expires_seconds > os.time() + 300 then + -- return true, value.access_token + -- end + -- end + + -- 如果 appid 和 appsecret 为空,则返回 nil + if appid == nil or appid == "" then + return false, "appid 为空" + end + if appsecret == nil or appsecret == "" then + return false, "appsecret 为空" + end + + + + -- 先检查有没有过期 + local cache_data = cache.get_json(tostring("wxofficial_info_"..appid)) + if cache_data then + if cache_data.update_time and cache_data.access_token then + -- 检查是否过期 + if os.time() - cache_data.update_time < 7200 then + return true,cache_data.access_token + end + end + end + + + + local result, data = httpclient.get(M.url_get_access_token .. "&appid=" .. appid .. "&secret=" .. appsecret) + if not result then + return false, data + end + + local body = cjson.decode(data) + if body.errcode ~= nil and body.errcode ~= 0 then + return false, body.errmsg + end + + + -- 更新 + cache.set_json(tostring("wxofficial_info_"..appid),{ + update_time = os.time(), + access_token = body.access_token + }) + return true,body.access_token + + +end + +-- 获取用户access_token +M.get_user_access_token = function(appid, appsecret, code) + local result, data = httpclient.get(M.url_get_user_access_token .. "&appid=" .. appid .. "&secret=" .. appsecret .. "&code=" .. code) + if not result then + return false, data + end + + local body = cjson.decode(data) + if body.errcode ~= nil and body.errcode ~= 0 then + return false, body.errmsg + end + return true, body +end + +-- 获取用户信息 +M.get_user_info = function(access_token, openid) + local url = M.url_get_user_info .. "&access_token=" .. access_token .. "&openid=" .. openid + local result, data = httpclient.get(url) + if not result then + return false, data + end + + local body = cjson.decode(data) + if body.errcode ~= nil and body.errcode ~= 0 then + return false, body.errmsg + end + return true, body +end + +-- 生成授权地址 +M.generate_auth_url = function(appid, redirect_uri) + return M.url_auth .. "?appid=" .. appid .. "&redirect_uri=" .. redirect_uri .. "&response_type=code&scope=snsapi_userinfo&state=1#wechat_redirect" +end + +-- 发送模板消息 +M.send_template_message = function(appid, appsecret, openid, template_id, jump_url, data) + local ok, access_token = M.get_access_token(appid, appsecret) + if not ok then + return false, access_token + end + local url = M.url_template_message .. "?access_token=" .. access_token + + local body = { + touser = openid, + template_id = template_id, + url = jump_url, + data = data, + appid = appid + } + local result, response = httpclient.post(url, { + ["Content-Type"] = "application/json" + }, cjson.encode(body)) + + if not result then + return false, response + end + local res_json = cjson.decode(response) + if res_json.errcode ~= 0 then + return false, res_json.errmsg + end + return true +end + + +-- 生成一次性订阅确认地址 +M.generate_subscribe_confirm_url = function(appid, scene, template_id, redirect_uri, reserved) + return "https://mp.weixin.qq.com/mp/subscribemsg?action=get_confirm&appid=" .. appid .. "&scene=" .. scene .. "&template_id=" .. template_id .. "&redirect_url=" .. redirect_uri .. "&#wechat_redirect" +end + +-------------------------------------------------------------------------------- +-- 以下为新增的部分,用于微信JS SDK 配置 + +-- 生成随机字符串函数 +local function generate_nonce_str(length) + --math.randomseed(os.time() + math.random()) -- 增加随机种子 + local chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + local res = {} + for i = 1, (length or 16) do + local rand = math.random(1, #chars) + res[#res+1] = string.sub(chars, rand, rand) + end + return table.concat(res) +end + +-- 获取 jsapi_ticket +M.get_jsapi_ticket = function(appid, appsecret) + local ticket_info = cache.get_json("wxjsapi_ticket") + if ticket_info then + if ticket_info.expires > os.time() + 300 then + return true, ticket_info.ticket + end + end + + local ok, access_token = M.get_access_token(appid, appsecret) + if not ok then + return false, access_token + end + + local ticket_url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" .. access_token .. "&type=jsapi" + local result, data = httpclient.get(ticket_url) + if not result then + return false, data + end + + local body = cjson.decode(data) + if body.errcode ~= 0 then + return false, body.errmsg + end + + cache.set_json("wxjsapi_ticket", { + ticket = body.ticket, + expires = os.time() + body.expires_in + }) + return true, body.ticket +end + +-- 获取微信JS SDK所需的配置参数 +-- 参数 current_url 必须是当前网页的完整 URL(不包含#号后的部分) +M.get_js_sdk_config = function(appid, appsecret, current_url) + local ok, jsapi_ticket = M.get_jsapi_ticket(appid, appsecret) + if not ok then + return false, jsapi_ticket..",err" + end + + local nonceStr = generate_nonce_str(16) + local timestamp = os.time() + + -- 拼接待签名字符串,注意顺序及格式严格按照微信要求 + local str = "jsapi_ticket=" .. jsapi_ticket .. "&noncestr=" .. nonceStr .. "×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 diff --git a/target/tencent/wxpay.lua b/target/tencent/wxpay.lua new file mode 100644 index 0000000..6a48d79 --- /dev/null +++ b/target/tencent/wxpay.lua @@ -0,0 +1,101 @@ +local http = require("fwutils.httpclient") + +local M = {} +-- 微信支付统一下单 +local url_pay = "https://api.mch.weixin.qq.com/pay/unifiedorder" + + +-- 纯文本方式取中间内容(不使用正则) +function extract_between(text, start_str, end_str) + local start_pos = string.find(text, start_str, 1, true) + if not start_pos then return nil end + + local from = start_pos + #start_str + local end_pos = string.find(text, end_str, from, true) + if not end_pos then return nil end + + return string.sub(text, from, end_pos - 1) +end + + +-- 验签 +M.sign = function(data,key) + local sign_str = "" + local keys = {} + for k,_ in pairs(data) do + table.insert(keys, k) + end + table.sort(keys) + for i,k in ipairs(keys) do + if i > 1 then + sign_str = sign_str .. "&" + end + sign_str = sign_str .. k .. "=" .. data[k] + end + sign_str = sign_str .. "&key=" .. key + return string.upper(codec.md5(sign_str)) +end + +-- 微信支付统一下单 +M.unified_order = function(appid,mch_id,openid,key,body,out_trade_no,total_fee,notify_url) + function xml_make(data) + local xml_data = "" + for k,v in pairs(data) do + xml_data = xml_data .. "<" .. k .. ">" .. v .. "" + end + xml_data = xml_data .. "" + 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,"") + if result_code ~= "SUCCESS" then + return false,extract_between(res,"") + end + + local prepay_id = extract_between(res,"") + + 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 + + diff --git a/target/utils.lua b/target/utils.lua new file mode 100644 index 0000000..47b038c --- /dev/null +++ b/target/utils.lua @@ -0,0 +1,286 @@ +local lfs = require("lfs") + +local M = {} + + +-- 创建目录 +M.create_dir = function(dirpath) + -- 检查操作系统类型 + local os_type = package.config:sub(1,1) + if os_type == "\\" then + -- Windows 系统 + os.execute("mkdir \"" .. dirpath:gsub("/", "\\") .. "\" /p") + else + -- Unix/Linux/Mac 系统 + os.execute("mkdir -p \"" .. dirpath .. "\"") + end +end +-- 转换为整数 +M.tointeger = function(data) + local num = tonumber(data) + if num == nil then + return nil + end + return math.floor(num) +end +-- 转为时间戳 +M.to_timestamp = function(time_str,pattern) + if pattern == nil then + pattern = "(%d+)%-(%d+)%-(%d+)%s+(%d+):(%d+):(%d+)" + end + local y, m, d, h, min, s = time_str:match(pattern) + local timestamp = os.time({ + year = tonumber(y), + month = tonumber(m), + day = tonumber(d), + hour = tonumber(h), + min = tonumber(min), + sec = tonumber(s) + }) + return timestamp +end +-- 取扩展名 +M.ext = function(filepath) + -- 取扩展名 + return string.match(filepath,"%.([^.]+)$") +end +-- 复制文件 +-- 增加第三个参数 replace,是否替换目标文件,默认为 false +M.copy_file = function(src, dst, replace) + replace = replace or false + -- 检查目标文件是否存在 + local dst_file = io.open(dst, "r") + if dst_file ~= nil then + dst_file:close() + if not replace then + return true + end + end + -- 打开源文件 + local file = io.open(src, "rb") + if not file then + print("ERR 2,src:",src) + return false + end + local content = file:read("*all") + file:close() + -- 写入目标文件 + local file = io.open(dst, "wb") + if not file then + print("ERR 3") + return false + end + file:write(content) + file:close() + return true +end +-- 取路径文件名(带扩展名) +M.filename = function(filepath) + return string.match(filepath,"[^/]+$") +end +-- 删除文件 +M.delete_file = function(filepath) + if os.remove(fw.website_dir()..filepath) ~= true then + return false + end + return true +end +-- 读取文件内容 +M.read_file = function(filepath) + local file = io.open(filepath, "rb") + if not file then + return nil, "无法打开文件: " .. tostring(filepath) + end + local content = file:read("*all") + file:close() + return content +end + +-- 保存内容到文件 +M.save_file = function(filepath, content) + local file = io.open(filepath, "wb") + if not file then + return false, "无法打开文件: " .. tostring(filepath) + end + file:write(content) + file:close() + return true +end +-- 是否存在文件 +M.exists_file = function(filepath) + local file = io.open(filepath, "rb") + if file then + file:close() + return true + else + return false + end +end +-- 取近N个月时间 +M.recent_months = function(n) + local mons = {} + local function format_time(y, m, d, h, i, s) + return string.format("%04d-%02d-%02d %02d:%02d:%02d", y, m, d, h, i, s) + end + local now = os.time() + local cur = os.date("*t", now) + for i = n-1, 0, -1 do + local year = cur.year + local month = cur.month - i + while month <= 0 do + month = month + 12 + year = year - 1 + end + -- 获取该月第一天与最后一天 + local first_day = format_time(year, month, 1, 0, 0, 0) + local next_month = month + 1 + local next_year = year + if next_month > 12 then + next_month = 1 + next_year = year + 1 + end + -- next_month 1号的前一天就是当前月最后一天 + local last_day_ts = os.time{year=next_year, month=next_month, day=1, hour=0, min=0, sec=0} - 1 + local last_day_tm = os.date("*t", last_day_ts) + local last_day = format_time(last_day_tm.year, last_day_tm.month, last_day_tm.day, 23, 59, 59) + local month_str = string.format("%04d-%02d", year, month) + table.insert(mons, month_str) + end + return mons +end +M.recent_months2 = function(n) + local mons = {} + local function format_time(y, m, d, h, i, s) + return string.format("%04d-%02d-%02d %02d:%02d:%02d", y, m, d, h, i, s) + end + local now = os.time() + local cur = os.date("*t", now) + for i = n-1, 0, -1 do + local year = cur.year + local month = cur.month - i + while month <= 0 do + month = month + 12 + year = year - 1 + end + -- 获取该月第一天与最后一天 + local first_day = format_time(year, month, 1, 0, 0, 0) + local next_month = month + 1 + local next_year = year + if next_month > 12 then + next_month = 1 + next_year = year + 1 + end + -- next_month 1号的前一天就是当前月最后一天 + local last_day_ts = os.time{year=next_year, month=next_month, day=1, hour=0, min=0, sec=0} - 1 + local last_day_tm = os.date("*t", last_day_ts) + local last_day = format_time(last_day_tm.year, last_day_tm.month, last_day_tm.day, 23, 59, 59) + local month_str = string.format("%04d-%02d", year, month) + table.insert(mons, { + month = month_str, + start_time = first_day, + end_time = last_day + }) + end + return mons +end +-- 十六进制转字节数组 +M.hex_to_bytes = function(hex_str) + -- 移除所有非十六进制字符 + hex_str = hex_str:gsub("[^%x]", ""):upper() + + -- 补0使长度为偶数 + if #hex_str % 2 == 1 then + hex_str = "0" .. hex_str + end + + -- 使用 gsub 一次性转换 + return (hex_str:gsub("(%x%x)", function(hex) + return string.char(tonumber(hex, 16)) + end)) +end +--[[ + 还原 Redis 字符串(去转义) + @param value 被转义的字符串 + @return 返回还原后的字符串 +]] +M.unescape_value = function(value) + if value == nil then + return nil + end + local str = tostring(value) + -- 如果是用双引号包住的,去掉包裹并恢复转义 + if #str >= 2 and string.sub(str,1,1) == "\"" and string.sub(str,-1,-1) == "\"" then + str = string.sub(str,2,-2) + str = string.gsub(str, "\\\"", "\"") + str = string.gsub(str, "\\\\", "\\") + return str + end + return str +end +-- 是否为静态资源扩展名 +M.is_static_ext_not_html = function(ext) + local static_exts = { + "jpg", + "jpeg", + "png", + "gif", + "bmp", + "ico", + "pdf", + "doc", + "docx", + "xls", + "xlsx", + "ppt", + "pptx", + "txt", + "css", + "js", + "json", + "xml", + "yaml", + "yml", + "zip", + "rar", + "7z", + "tar", + "gz", + "bz2", + "xz", + "mp3", + "wav", + "ogg", + "aac", + "m4a", + "mp4", + "avi", + "mov", + "wmv", + "flv", + "webm", + "mkv", + "avi", + "mov", + "wmv", + "flv", + "webm", + "mkv", + } + for _, v in ipairs(static_exts) do + if v == ext then + return true + end + end + return false +end +-- 遍历目录 +M.traverse_dir = function(dirpath) + local files = {} + for file in lfs.dir(dirpath) do + if file ~= "." and file ~= ".." then + table.insert(files, file) + end + end + return files +end +return M \ No newline at end of file