├── dist.ini ├── lib └── resty │ ├── tofu.lua │ └── tofu │ ├── aes.lua │ ├── crontab.lua │ ├── ctx.lua │ ├── extend │ ├── builtin.lua │ ├── cache.lua │ ├── config.lua │ ├── jili.lua │ ├── log.lua │ ├── model.lua │ ├── session.lua │ ├── task.lua │ └── view.lua │ ├── inotify.lua │ ├── mate.lua │ ├── middleware │ ├── controller.lua │ ├── guard.lua │ ├── payload.lua │ ├── router.lua │ └── trace.lua │ ├── model-mysql-helper.lua │ ├── radio.lua │ ├── refile.lua │ ├── util.lua │ ├── validation.lua │ ├── wscontroller.lua │ └── x6.lua └── readme.md /dist.ini: -------------------------------------------------------------------------------- 1 | name=lua-resty-tofu 2 | abstract=(alpha) a modern framework 3 | version=0.1.19 4 | author=d 5 | is_original=yes 6 | license=mit 7 | lib_dir=lib 8 | doc_dir=lib 9 | repo_link=https://github.com/d80x86/lua-resty-tofu 10 | main_module=lib/resty/tofu.lua 11 | -------------------------------------------------------------------------------- /lib/resty/tofu.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @file tofu.lua 3 | -- @author d 4 | -- 5 | -- 6 | 7 | 8 | local _TOFU_PATH = debug.getinfo(1, 'S').source:sub(2,-1):match('^.*/') 9 | -- package.path = package.path .. _TOFU_PATH .. '?.lua;' 10 | 11 | 12 | -- 13 | -- 一些缺省配置 14 | -- 15 | local _options = { 16 | env = 'production', -- | development 17 | app_path = 'lua/', 18 | env_uesr = 'nginx', 19 | } 20 | 21 | 22 | 23 | local _mate = require 'resty.tofu.mate' 24 | local _M = { _VERSION = _mate._VERSION } 25 | local _tofu = _mate.tofu 26 | local _core = _mate.core 27 | 28 | _tofu.VERSION = _mate._VERSION 29 | _tofu.TOFU_PATH = _TOFU_PATH 30 | _tofu.ROOT_PATH = ngx.config.prefix() 31 | _tofu.APP_PATH = _tofu.ROOT_PATH .. _options.app_path 32 | _tofu.ENV = os.getenv('NGX_ENV') or _options.env 33 | _tofu.ENV_USER = os.getenv('USER') or _options.env_user 34 | 35 | -- ---------------------------------------------------- 36 | -- hack 37 | -- 38 | -- 把 tofu 设置为全局 39 | local new_env = getfenv(0) 40 | local old_mt = getmetatable(new_env) 41 | setmetatable(new_env, {}) 42 | new_env.tofu = _tofu 43 | -- 修复第三方库使用全局变量_问题 44 | new_env._ = '' 45 | setmetatable(new_env, old_mt) 46 | 47 | 48 | -- 49 | -- 重写 error 50 | -- 只为更友好地显示错误 51 | -- 52 | local _log = require 'resty.tofu.extend.log'.ngxlog() 53 | local function error(msg) 54 | _log.e(msg) 55 | ngx.exit() 56 | -- os.exit() 57 | end 58 | 59 | 60 | 61 | -- 62 | -- 读取 conf 下的配置 63 | -- 64 | local _conf = require 'resty.tofu.extend.config'._install({ 65 | env = _tofu.ENV, 66 | prefix = _tofu.ROOT_PATH .. 'conf/', 67 | }) 68 | 69 | 70 | -- --------------------------------------------------- 71 | -- load extend 72 | -- 73 | -- 74 | do -- load extend begin -- 75 | 76 | local _string_sub = string.sub 77 | 78 | 79 | local function _extend_install(named, handle, ...) 80 | if '' == named then 81 | error('extend named cannot be null("") ') 82 | end 83 | if _tofu[named] then 84 | error('extend '..named.. ' is already installed') 85 | end 86 | if 'string' == type(handle) then 87 | local src = _tofu.APP_PATH .. 'extend/'.. string.gsub(handle, '%.', '/').. '.lua' 88 | local ok, res = pcall(dofile, src) 89 | if not ok then 90 | if 'cannot open' == res:sub(1, 11) then 91 | ok, res = pcall(require, handle) 92 | if not ok then 93 | error('extend ' .. handle .. ' : ' .. res) 94 | end 95 | else 96 | error(res) 97 | end 98 | end 99 | handle = res 100 | end 101 | 102 | if 'function' == type(handle) then 103 | if not named or '' == named then 104 | error('extend named cannot be null("") ') 105 | end 106 | _tofu[named] = setfenv(handle, new_env)(...) 107 | elseif 'table' == type(handle) then 108 | if named then 109 | _tofu[named] = handle._install and handle._install(...) or handle 110 | else 111 | for k, v in pairs(handle) do 112 | if '_' ~= _string_sub(k,1,1) then -- 忽略下划线开头的字段 113 | _tofu[k] = nil == _tofu[k] and v -- 不能重复 114 | or error ('tofu.' .. k .. ' is already') 115 | end 116 | end 117 | end 118 | else 119 | error('load extend ' .. named .. ' error') 120 | end 121 | end 122 | 123 | 124 | -- 125 | -- load extend from config 126 | -- 127 | -- { 128 | -- { 129 | -- enable = bool default true 130 | -- named = 'string', 131 | -- [type] = | default, 132 | -- = { 133 | -- handle = string | function | table<._install> 134 | -- options = { ... } 135 | -- } 136 | -- }, 137 | -- ... 138 | -- } 139 | -- 140 | 141 | -- 142 | -- 读取配置且合并 143 | -- 因为extend是个数组,这里特处理配置,使其支持只覆盖其中某项配置 144 | -- 145 | -- 146 | -- 147 | -- 合并配置的辅佐函数 148 | -- 149 | local _tab_merge = require 'resty.tofu.util'.tab_merge 150 | 151 | local _ext_seq = {} 152 | local _ext_map = {} 153 | local function _merge_extend_conf(extend) 154 | if not extend then 155 | return 156 | end 157 | for i, v in ipairs(extend) do 158 | if 'table' == type(v) then 159 | local named = v.named or (#_ext_seq + 1) -- 如果没有命名,则使用顺序号 160 | local e =_ext_map[named] 161 | if not e then 162 | e = {} 163 | _ext_seq[#_ext_seq + 1] = named 164 | _ext_map[named] = e 165 | end 166 | _tab_merge(e, v) 167 | 168 | elseif 'string' == type(v) then 169 | local named = v 170 | if not _ext_map[named] then 171 | _ext_seq[#_ext_seq + 1] = named 172 | _ext_map[named] = v 173 | end 174 | end 175 | end 176 | end 177 | 178 | -- 加载配置 179 | local _cf = require 'resty.tofu.extend.config' 180 | local _tmp1 = _cf.read(_tofu.ROOT_PATH .. 'conf/extend', {}) or {} 181 | local _tmp2 = _cf.read(_tofu.ROOT_PATH .. 'conf/extend.' .. tofu.ENV) or {} 182 | 183 | _merge_extend_conf(_tmp1.extend) 184 | _merge_extend_conf(_tmp2.extend) 185 | 186 | -- 按顺序copy配置 187 | local extend = {} 188 | for _, named in ipairs(_ext_seq) do 189 | extend[#extend+1] = _ext_map[named] 190 | end 191 | 192 | -- local extend = _conf.extend or {} 193 | for _, ext in ipairs(extend) do 194 | if 'string' == type (ext) then 195 | _extend_install(nil, ext) 196 | else 197 | if false ~= ext.enable then 198 | local item = ext[ext.type or 'default'] 199 | _extend_install(ext.named, item.handle, item.options) 200 | end 201 | end 202 | end 203 | 204 | 205 | end -- load extend end -- 206 | 207 | 208 | 209 | 210 | 211 | -- -------------------------------------------------------- 212 | -- load middleware 213 | -- 214 | -- 215 | do -- load middleware begin -- 216 | 217 | _core._app = require 'resty.tofu.x6' .new() 218 | 219 | 220 | -- 选项 match 处理 221 | local _mid_warp = function(match, handle) 222 | if not match then 223 | return handle 224 | end 225 | 226 | -- match 为正则方式 227 | if 'string' == type(match) then 228 | local re_match = ngx.re.match 229 | return function(ctx, flow) 230 | local ok = re_match(ngx.var.uri, match, 'jo') 231 | if ok then 232 | return handle(ctx, flow) 233 | else 234 | return flow() 235 | end 236 | end 237 | 238 | -- match 为函数方式 239 | elseif 'function' == type(match) then 240 | return function(ctx, flow) 241 | local ok = match(ctx) 242 | if ok then 243 | return handle(ctx, flow) 244 | else 245 | return flow() 246 | end 247 | end 248 | end 249 | end 250 | 251 | -- 252 | -- 253 | -- 254 | local function _middleware_install(mid) 255 | local handle = mid.handle 256 | local app = _core._app 257 | if 'string' == type(handle) then 258 | local src = _tofu.APP_PATH..'middleware/'..string.gsub(handle, '%.', '/')..'.lua' 259 | local ok, res = pcall(dofile, src) 260 | if not ok then 261 | if 'cannot open' == res:sub(1, 11) then 262 | ok, res = pcall(require, handle) 263 | if not ok then 264 | if string.find(res, 'not found', 1, true) then 265 | error('middleware ' .. handle .. ' not found', 2) 266 | else 267 | error(res) 268 | end 269 | end 270 | else 271 | error(res) 272 | end 273 | end 274 | handle = res 275 | end 276 | 277 | if 'function' == type(handle) then 278 | local h = setfenv(handle, new_env)(mid.options) 279 | local fn = _mid_warp(mid.match, h) 280 | app:use(fn) 281 | elseif 'table' == type(handle) then 282 | local h = setfenv(handle.new, new_env)(mid.options) 283 | local fn = _mid_warp(mid.match, h) 284 | app:use(fn) 285 | else 286 | error('load middleware ' .. handle .. ' error') 287 | end 288 | end 289 | 290 | 291 | 292 | -- 293 | -- load middleware from config 294 | -- 295 | -- { 296 | -- { 297 | -- enable = bool default:true 298 | -- handle = function | string | table<.new> 299 | -- options = { ... } 300 | -- }, 301 | -- ... 302 | -- } 303 | -- 304 | local middleware = _conf.middleware or {} 305 | for _, mid in ipairs(middleware) do 306 | if 'string' == type(mid) then 307 | mid = {handle = mid} 308 | end 309 | if false ~= mid.enable then 310 | -- _middleware_install(mid.handle, mid.options) 311 | _middleware_install(mid) 312 | end 313 | end 314 | 315 | 316 | end -- load middleware end -- 317 | 318 | 319 | 320 | 321 | -- ----------------------------------------------------- 322 | -- 323 | -- 324 | if 0 == ngx.worker.id() and _tofu.log then 325 | _tofu.log.n('tofu version:', _M._VERSION) 326 | _tofu.log.n('environment:', _tofu.ENV) 327 | if _conf.ngx_port then 328 | _tofu.log.n('listen:', _conf.ngx_port) 329 | end 330 | end 331 | 332 | 333 | 334 | -- ------------------------------------------------------- 335 | -- 336 | -- 337 | setmetatable(_tofu, old_mt) 338 | -- ----- tofu setup end --------------- 339 | -- 340 | 341 | 342 | 343 | 344 | -- ------------------------------------------------- 345 | -- api 346 | -- 347 | local _ctx = require 'resty.tofu.ctx' 348 | 349 | function _M.start() 350 | local ctx = _ctx.stash() 351 | local ok, err = pcall(_core._app.handle, _core._app, ctx) 352 | if not ok then 353 | if _tofu.log then 354 | _tofu.log.e(err) 355 | else 356 | ngx.log(ngx.ERR, err) 357 | end 358 | ngx.status = 500 359 | end 360 | _ctx.apply() 361 | ngx.exit(ngx.status) 362 | end 363 | 364 | 365 | 366 | return _M 367 | 368 | 369 | -------------------------------------------------------------------------------- /lib/resty/tofu/aes.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @file aes.lua 3 | -- @author d 4 | -- @version 0.1.0 5 | -- @brief 扩展 resty.aes 支持 aes/gcm 模式 6 | -- 7 | 8 | local _M = require 'resty.aes' 9 | 10 | local _ffi = require 'ffi' 11 | local _ffi_c = _ffi.C 12 | local _ffi_new = _ffi.new 13 | local _ffi_str = _ffi.string 14 | 15 | 16 | -- 17 | -- 增加 gcm 支持 18 | -- 19 | _ffi.cdef [[ 20 | 21 | const EVP_CIPHER *EVP_aes_128_gcm(void); 22 | const EVP_CIPHER *EVP_aes_256_gcm(void); 23 | 24 | ]] 25 | 26 | 27 | -- 28 | -- 重写 decrypt 方法 29 | -- 30 | function _M:decrypt(s) 31 | local s_len = #s 32 | local buf = _ffi_new("unsigned char[?]", s_len) 33 | local out_len = _ffi_new("int[1]") 34 | local tmp_len = _ffi_new("int[1]") 35 | local ctx = self._decrypt_ctx 36 | 37 | if _ffi_c.EVP_DecryptInit_ex(ctx, nil, nil, nil, nil) == 0 then 38 | return nil 39 | end 40 | 41 | if _ffi_c.EVP_DecryptUpdate(ctx, buf, out_len, s, s_len) == 0 then 42 | return nil 43 | end 44 | 45 | _ffi_c.EVP_DecryptFinal_ex(ctx, buf + out_len[0], tmp_len) 46 | return _ffi_str(buf, out_len[0] + tmp_len[0]) 47 | end 48 | 49 | 50 | 51 | return _M 52 | -------------------------------------------------------------------------------- /lib/resty/tofu/crontab.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @file crontab.lua 3 | -- @author d 4 | -- @version 0.1.0 5 | -- @brief 定时任务服务 6 | -- 7 | 8 | 9 | local _os_time = os.time 10 | local _os_date = os.date 11 | local _gmatch = string.gmatch 12 | 13 | 14 | local _M = { _VERSION = '0.1.0' } 15 | 16 | 17 | 18 | 19 | local _opts = {} 20 | 21 | 22 | -- 23 | -- 24 | -- 25 | local function _check_positive_int(n, v) 26 | if 'number' ~= type(v) or v < 0 then 27 | error (n .. ' must be a positive number') 28 | end 29 | end 30 | 31 | 32 | 33 | local function _is_callable(f) 34 | local t = type(f) 35 | 36 | if 'function' == t then 37 | return true 38 | end 39 | 40 | if 'table' == t then 41 | local mt = getmetatable(f) 42 | return 'table' == type(mt) and 'function' == type(mt.__call) 43 | end 44 | 45 | return false 46 | end 47 | 48 | 49 | 50 | 51 | -- 52 | -- 53 | -- 54 | local function _cron_calc(item, v) 55 | for r in _gmatch(item, '[^,]+') do 56 | -- 特殊 * 57 | do 58 | if '*' == r then 59 | return true, 0 60 | end 61 | end 62 | 63 | -- 每 */n 64 | do 65 | local _, _, a = string.find(r, '%*/(%d+)') 66 | if a then 67 | return true, tonumber(a) or 0 68 | end 69 | end 70 | 71 | -- 准点 a 72 | do 73 | local a = tonumber(r) 74 | if 'number' == type(a) and a == v then 75 | return true, 0 76 | end 77 | end 78 | 79 | -- 范围 a-b/c 80 | do 81 | local _, _, a, b, c = string.find(r, '(%d+)%-(%d+)/?(%d*)') 82 | if a and b and tonumber(a) <= v and v <= tonumber(b) then 83 | return true, tonumber(c) or 0 84 | end 85 | end 86 | 87 | end 88 | 89 | return false, 0 90 | end 91 | 92 | -- 93 | -- 94 | -- 95 | local function _cron_test(self) 96 | local tm = self.time 97 | local now = _os_date('*t') 98 | for _, k in ipairs {'min', 'hour', 'day', 'month', 'wday'} do 99 | local ok, v = _cron_calc(tm[k], now[k]) 100 | if not ok then 101 | return false 102 | end 103 | if 0 < v then 104 | local run = self.running 105 | run[k] = (run[k] or -1) + 1 -- -1: 抵消第一次触发时间问题 106 | if run[k] ~= v then 107 | self.lst = _os_time() 108 | return false 109 | else 110 | run[k] = 0 111 | end 112 | end 113 | end 114 | return true 115 | end 116 | 117 | 118 | 119 | -- @param 120 | -- update 121 | -- time, 122 | -- callback, 123 | -- ...args, 124 | -- 125 | -- } 126 | local function _clock_new(update, time, callback, ...) 127 | assert(update) 128 | assert(time) 129 | assert(_is_callable(callback), "callback must be a function") 130 | return { 131 | time = time, 132 | callback = callback, 133 | args = {...}, 134 | running = 0, 135 | update = update, 136 | lst = 0, 137 | } 138 | end 139 | 140 | 141 | 142 | local function _clock_update_after(self, dt) 143 | if self.time < self.running then return true end 144 | self.running = self.running + dt 145 | if self.time <= self.running then 146 | self.callback(unpack(self.args)) 147 | return true 148 | end 149 | end 150 | 151 | local function _clock_update_every(self, dt) 152 | self.running = self.running + dt 153 | if self.time <= self.running then 154 | self.callback(unpack(self.args)) 155 | self.running = self.running - self.time 156 | end 157 | end 158 | 159 | local function _clock_update_cron(self, _dt) 160 | local now_time = _os_time() 161 | local tm = self.time 162 | -- 要求间隔在于1分钟 163 | if 59 < now_time - self.lst and _cron_test(self) then 164 | self.callback(unpack(self.args)) 165 | self.lst = now_time 166 | end 167 | end 168 | 169 | local function _clock_reset(self, dt) 170 | dt = dt or 0 171 | _check_positive_int('dt', dt) 172 | self.running = dt 173 | end 174 | 175 | 176 | local function _clock_new_after(...) 177 | return _clock_new(_clock_update_after, ...) 178 | end 179 | 180 | 181 | local function _clock_new_every(...) 182 | return _clock_new(_clock_update_every, ...) 183 | end 184 | 185 | 186 | local function _clock_new_cron(...) 187 | local clock = _clock_new(_clock_update_cron, ...) 188 | clock.running = {} 189 | return clock 190 | end 191 | 192 | 193 | 194 | 195 | -- ----------------------------------------- 196 | -- 197 | -- 198 | 199 | 200 | 201 | local _is_runing = false 202 | local _clocks = {} 203 | local _last_time = _os_time() 204 | 205 | 206 | -- 207 | -- 208 | -- 209 | local function _isworker(id) 210 | return ngx.worker.id() == (id or 0) 211 | end 212 | 213 | 214 | -- 215 | -- 216 | -- 217 | local function _update(dt) 218 | for id, c in pairs(_clocks) do 219 | if c:update(dt) then 220 | _clocks[id] = nil 221 | end 222 | end 223 | end 224 | 225 | 226 | -- 227 | -- 228 | -- 229 | local function _loop() 230 | local t = _os_time() 231 | _update(t - _last_time) 232 | _last_time = t 233 | _is_runing = ngx.timer.at(1, _loop) 234 | 235 | end 236 | 237 | 238 | -- 239 | -- 240 | -- 241 | function _M.start() 242 | -- if not _isworker() then return end 243 | if _is_runing then 244 | return 245 | end 246 | _last_time = _os_time() 247 | -- _loop() 248 | _is_runing = ngx.timer.at(0, _loop) 249 | end 250 | 251 | 252 | -- 253 | -- 254 | -- 255 | function _M.stop() 256 | -- if not _isworker() then return end 257 | if _is_runing then 258 | ngx.thread.kill(_is_runing) 259 | _is_runing = false 260 | end 261 | end 262 | 263 | 264 | -- @param [named],second, handle, ... 265 | function _M.add_after(p1, p2, p3, ...) 266 | -- #3 267 | if 'string' == type(p1) then 268 | _check_positive_int('after', p2) 269 | local named = p1 270 | _clocks[named] = _clock_new_after(p2, p3, ...) 271 | return named 272 | 273 | -- #2 274 | else 275 | _check_positive_int('dt', p1) 276 | local named = #_clocks + 1 277 | _clocks[named] = _clock_new_after(p1, p2, p3, ...) 278 | return named 279 | end 280 | end 281 | 282 | 283 | 284 | -- @param [named], interval, handle, ... 285 | function _M.add_every(p1, p2, p3, ...) 286 | -- #3 287 | if 'string' == type(p1) then 288 | _check_positive_int('interval', p2) 289 | local named = p1 290 | _clocks[named] = _clock_new_every(p2, p3, ...) 291 | return named 292 | 293 | -- #2 294 | else 295 | _check_positive_int('dt', p1) 296 | local named = #_clocks + 1 297 | _clocks[named] = _clock_new_every(p1, p2, p3, ...) 298 | return named 299 | end 300 | end 301 | 302 | 303 | 304 | -- 305 | -- @param [named], cron, handle, ... 306 | function _M.add_cron(p1, p2, p3, ...) 307 | local named, cron, handle, n = p1, p2, p3, 3 308 | if 'string' == type(p1) and _is_callable(p2) then 309 | named = #_clocks + 1 310 | cron = p1 311 | handle = p2 312 | n = 2 313 | end 314 | 315 | local t = {} 316 | for v in _gmatch(cron, '[%d/,%-*]+') do 317 | local i = tonumber(v) or v 318 | t[#t+1] = i 319 | end 320 | if 5 ~= #t then 321 | error ( string.format('crontab string "%s" error', cron)) 322 | end 323 | local cron_time = { 324 | min = t[1], 325 | hour = t[2], 326 | day = t[3], 327 | month = t[4], 328 | wday = t[5], 329 | } 330 | 331 | -- #3 332 | if 3 == n then 333 | _clocks[named] = _clock_new_cron(cron_time, handle, ...) 334 | 335 | -- #2 336 | else 337 | _clocks[named] = _clock_new_cron(cron_time, handle, p3, ...) 338 | end 339 | 340 | return named 341 | end 342 | 343 | 344 | function _M.rm(id) 345 | end 346 | 347 | 348 | 349 | return _M 350 | 351 | -------------------------------------------------------------------------------- /lib/resty/tofu/ctx.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @file ctx.lua 3 | -- @author d 4 | -- 5 | 6 | 7 | local _M = { _VERSION = '0.1.0' } 8 | 9 | 10 | local _ctxs = {} 11 | 12 | 13 | function _M.stash() 14 | local ref = ngx.var.request_id 15 | local t = _ctxs[ref] 16 | if not t then 17 | t = {} 18 | _ctxs[ref] = t 19 | end 20 | return t 21 | end 22 | 23 | 24 | function _M.fetch(ref) 25 | ref = ref or ngx.var.request_id 26 | return _ctxs[ref] 27 | end 28 | 29 | 30 | 31 | function _M.apply() 32 | local ref = ngx.var.request_id 33 | _ctxs[ref] = nil 34 | end 35 | 36 | 37 | function _M.new() 38 | return _M 39 | end 40 | 41 | 42 | return _M 43 | -------------------------------------------------------------------------------- /lib/resty/tofu/extend/builtin.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @file builtin.lua 3 | -- @author d 4 | -- @version 0.1.1 5 | -- 6 | 7 | local _json = require 'cjson.safe' 8 | 9 | local _tofu = require 'resty.tofu.mate'.tofu 10 | local _ctx = require 'resty.tofu.ctx' 11 | local _util = require 'resty.tofu.util' 12 | local _merge = _util.tab_merge 13 | local _isempty = _util.isempty 14 | 15 | 16 | 17 | 18 | local _M = {_VERSION='0.1.1'} 19 | 20 | 21 | 22 | 23 | local _options = { 24 | state_field = 'errno', 25 | message_field = 'errmsg', 26 | data_field = 'data', 27 | default_state = 0, 28 | } 29 | 30 | local _conf = _util.conf_load(_tofu.ROOT_PATH .. 'conf/config.lua') 31 | local _conf_env = _util.conf_load(_tofu.ROOT_PATH .. 'conf/config.'.. tofu.ENV ..'.lua') 32 | _merge(_options, _conf, _conf_env) 33 | 34 | 35 | 36 | -- 37 | -- 获取请求参 (优先级 post > get > uri ) 38 | -- 依赖中间件: tofu.middlewate.payload 39 | -- 40 | function _M.args(name) 41 | local args = _ctx.fetch().args 42 | 43 | if name then 44 | -- return args[name] 45 | return args(name) 46 | end 47 | 48 | return args 49 | end 50 | 51 | 52 | function _M.response(sts, msg, data) 53 | if _isempty(ngx.header['Content-Type']) then 54 | ngx.header['Content-Type'] = 'application/json' 55 | end 56 | local result = { 57 | [_options.state_field] = sts or _options.default_state, 58 | [_options.message_field] = msg or '', 59 | [_options.data_field] = data, 60 | } 61 | ngx.print(_json.encode(result)) 62 | end 63 | 64 | 65 | -- 66 | -- 67 | -- 68 | function _M.success(data, msg) 69 | _M.response(_options.default_state, msg, data) 70 | return false 71 | end 72 | 73 | 74 | 75 | -- 76 | -- 77 | -- 78 | function _M.fail(sts, msg, data) 79 | _M.response(sts, msg, data) 80 | return false 81 | end 82 | 83 | 84 | 85 | 86 | return _M 87 | 88 | 89 | -------------------------------------------------------------------------------- /lib/resty/tofu/extend/cache.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @author d 3 | -- @version 0.1.1 4 | -- @brief cache (shm) 5 | -- 6 | 7 | 8 | local _resty_lock = require 'resty.lock' 9 | local _util = require 'resty.tofu.util' 10 | local _encode = require 'cjson.safe'.encode 11 | local _decode = require 'cjson.safe'.decode 12 | 13 | local _M = { _VERSION = '0.1.1' } 14 | 15 | 16 | local _opts = { 17 | ttl = 90 * 60, -- 秒 -- 90分钟 18 | shm = 'tofu_cache_dict', -- lua_shared_dict 配置 19 | timeout = 5, 20 | } 21 | 22 | 23 | local _KEY_ = 'cache:' 24 | local _store 25 | 26 | 27 | local function _get_and_lock(key) 28 | local lock, err = _resty_lock:new(_opts.shm, {timeout = _opts.timeout or 5}) 29 | if not lock then 30 | return error (err) 31 | end 32 | local elapsed, err = lock:lock('lock:'..key) 33 | if not elapsed then 34 | return error (err) 35 | end 36 | return lock 37 | end 38 | 39 | 40 | 41 | -- 42 | -- 43 | -- 44 | function _M._install(opts) 45 | _util.tab_merge(_opts, opts) 46 | _store = ngx.shared[_opts.shm] 47 | if not _store then 48 | return error ('extend cache shm need config lua_shared_dict name on tofu.nginx.conf') 49 | end 50 | return _M 51 | end 52 | 53 | 54 | -- ---------------------------- 55 | -- api 56 | -- 57 | 58 | -- 获取cache的值 59 | -- @param key string 60 | -- @param init value | function() 61 | -- @param ... init(...) v 62 | -- 63 | function _M.get(key, init, ...) 64 | if not key then 65 | return error 'key error' 66 | end 67 | key = _KEY_ .. key 68 | 69 | local val = _store:get(key) 70 | if nil ~= val then 71 | return _decode(val) 72 | end 73 | 74 | local lock = _get_and_lock(key) 75 | val = _store:get(key) 76 | if nil ~= val then 77 | lock:unlock() 78 | return _decode(val) 79 | end 80 | 81 | if 'function' == type(init) then 82 | val = init(...) 83 | else 84 | val = init 85 | end 86 | 87 | if nil ~= val then 88 | local ok, err = _store:set(key, _encode(val), _opts.ttl) 89 | if not ok then 90 | lock:unlock() 91 | error (err) -- 'no memory' 92 | return val, err 93 | end 94 | end 95 | 96 | lock:unlock() 97 | return val 98 | end 99 | 100 | 101 | -- 102 | -- 103 | -- 104 | function _M.set(key, val, ttl) 105 | if not key then 106 | return error 'key error' 107 | end 108 | key = _KEY_ .. key 109 | local ok, err = _store:set(key, _encode(val), ttl or _opts.ttl) 110 | if not ok then 111 | error (err) 112 | end 113 | end 114 | 115 | 116 | -- 117 | -- 118 | -- 119 | function _M.del(key) 120 | key = _KEY_ .. key 121 | _store:delete(key) 122 | end 123 | 124 | 125 | 126 | -- 127 | -- 128 | -- 129 | function _M.incr(key, val, init, ttl) 130 | if not key then 131 | error 'key error' 132 | end 133 | key = _KEY_ .. key 134 | local newval, err = _store:incr(key, val, init, ttl or _opts.ttl) 135 | if err then 136 | error (err) 137 | end 138 | return newval 139 | end 140 | 141 | 142 | -- 143 | -- 144 | -- 145 | function _M.capacity() 146 | return _store:capacity() 147 | end 148 | 149 | 150 | return _M 151 | 152 | -------------------------------------------------------------------------------- /lib/resty/tofu/extend/config.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @file conf.lua 3 | -- @author d 4 | -- @brief 加载lua语法的配置文件 5 | -- 6 | 7 | 8 | local _util = require 'resty.tofu.util' 9 | local _conf_load = _util.conf_load 10 | local _tab_merge = _util.tab_merge 11 | 12 | 13 | 14 | local _M = { _VERSION = '0.1.1'} 15 | 16 | 17 | 18 | -- 19 | -- 缺省配置 20 | -- 21 | local _opts = { 22 | prefix = 'conf', 23 | env = nil, 24 | default = 'config', 25 | } 26 | 27 | 28 | 29 | 30 | -- 31 | -- 32 | -- 33 | local function _load(conf_file, opts) 34 | opts = opts or {} 35 | if opts.prefix then 36 | conf_file = opts.prefix .. '/' .. conf_file 37 | end 38 | local res, err = _conf_load(conf_file .. '.lua') 39 | if err and 'cannot open' ~= err:sub(1, 11) then 40 | return nil, err 41 | end 42 | 43 | local conf = res 44 | 45 | if opts.env then 46 | res, err = _conf_load(conf_file .. '.' .. opts.env .. '.lua') 47 | if res then 48 | _tab_merge(conf, res) 49 | elseif err and 'cannot open' ~= err:sub(1, 11) then 50 | return nil, err 51 | end 52 | end 53 | 54 | return conf 55 | end 56 | 57 | 58 | 59 | 60 | -- 61 | -- 62 | -- 63 | function _M._install(opts) 64 | opts = _tab_merge({}, _opts, opts) 65 | 66 | -- cache 67 | local _conf = {} 68 | local _ready = {} 69 | 70 | local __index = function(t, key) 71 | local v = rawget(_conf, key) 72 | if not v and not rawget(_ready, key) then 73 | local conf, err = _load(key, opts) 74 | if conf then 75 | _tab_merge(_conf, conf) 76 | v = rawget(_conf, key) 77 | rawset(_ready, key, true) 78 | elseif err then 79 | (tofu and tofu.log and tofu.log.e or error)(err) 80 | end 81 | end 82 | return v 83 | end 84 | 85 | local __newindex = function(t, key, v) 86 | rawset(_conf, key, v) 87 | rawset(_ready, key, true) 88 | end 89 | 90 | -- preload config 91 | __index(opts, opts.default) 92 | 93 | return setmetatable({}, {__index = __index, __newindex = __newindex}) 94 | end 95 | 96 | 97 | 98 | -- 99 | -- 100 | -- 101 | function _M.read(conf_file, opts) 102 | opts = opts or {} 103 | return _load(conf_file, opts) 104 | end 105 | 106 | 107 | -- 108 | -- 109 | -- 110 | function _M.new(opts) 111 | return _M._install(opts) 112 | end 113 | 114 | 115 | 116 | return _M 117 | 118 | -------------------------------------------------------------------------------- /lib/resty/tofu/extend/jili.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- tofu 扩展 3 | -- @file jili.lua 4 | -- @author d 5 | -- @brief 一个watcher(开发使用), 当文件发生改变时,重启或加载文件 6 | -- 7 | 8 | local _process = require 'ngx.process' 9 | local _shell = require 'resty.shell' 10 | 11 | local _cli_opts = require 'resty.tofu.cli.opts' 12 | local _util = require 'resty.tofu.util' 13 | local _refile = require 'resty.tofu.refile' 14 | local _inotify = require 'resty.tofu.inotify' 15 | local _watcher = _inotify.new() 16 | local _IN = _inotify.IN 17 | local _band = require 'bit'.band 18 | local _match = string.match 19 | local _find = string.find 20 | 21 | local _is_show = 0 == ngx.worker.id() 22 | 23 | 24 | -- 25 | -- 缺省设置 26 | -- 27 | local _opts = { 28 | -- 是否log 29 | trace = true, 30 | 31 | -- {监视的路径,与处理方式} 32 | path = { 33 | -- string | {path:string, ngx_restart | reload | auto_restart} 34 | {tofu.ROOT_PATH .. 'conf', 'ngx_restart'}, 35 | {tofu.APP_PATH .. 'controller', 'auto_restart', '/_%w+%.lua$'}, 36 | {tofu.APP_PATH .. 'middleware', 'ngx_restart'}, 37 | {tofu.APP_PATH .. 'task', 'ngx_restart'}, 38 | {tofu.APP_PATH .. 'model', 'auto_restart', '/_%w+%.lua$'}, 39 | 40 | tofu.APP_PATH, -- default processor: ngx_restart 41 | }, 42 | } 43 | 44 | 45 | 46 | 47 | local _plan = { 48 | -- path, handle 49 | -- {path, processor} 50 | } 51 | 52 | 53 | local _plan_handle = { } 54 | 55 | -- 56 | -- @param m MOD | REM : 修改 | 移除 57 | -- 58 | 59 | -- 60 | -- 61 | -- 62 | function _plan_handle.reload(f, m) 63 | if 'MOD' == m then 64 | return _refile.reload(f) 65 | else 66 | return _refile.remove(f) 67 | end 68 | end 69 | 70 | 71 | -- 72 | -- 73 | -- 74 | function _plan_handle.ngx_reload(f, m) 75 | -- 只处理 lua 文件 76 | if not _match(f, '%.lua$') then return true end 77 | local pid = _process.get_master_pid() 78 | os.execute('kill -HUP ' .. pid) 79 | return true 80 | end 81 | 82 | 83 | -- 84 | -- 85 | -- 86 | function _plan_handle.ngx_restart() 87 | if 0 ~= ngx.worker.id() then 88 | return true 89 | end 90 | 91 | local pid = _process.get_master_pid() 92 | _cli_opts._init({}) 93 | os.execute('kill -HUP ' .. pid) 94 | return true 95 | end 96 | 97 | 98 | -- 99 | -- @param reg 正则匹配 100 | -- 101 | function _plan_handle.auto_restart(f, m, reg) 102 | local ok, err = _plan_handle.reload(f, m) 103 | if not ok then 104 | return nil, err 105 | end 106 | 107 | if reg and _match(f, reg) then 108 | return _plan_handle.ngx_restart() 109 | end 110 | 111 | return true 112 | end 113 | 114 | 115 | -- 116 | -- 117 | -- 118 | _plan_handle.default = _plan_handle.ngx_reload 119 | 120 | 121 | 122 | 123 | -- 124 | -- 处理lua文件变化 125 | -- 126 | local function _handle(iev) 127 | if iev.isdir then return end 128 | local ph = _plan_handle.default 129 | local arg = nil 130 | for _, p in ipairs(_plan) do 131 | if _find(iev.path, p[1], 1, true) then 132 | ph = _plan_handle[p[2]] 133 | arg = p[3] 134 | if not ph then 135 | tofu.log.e('not found plan handleer:', p, iev) 136 | return 137 | end 138 | break 139 | end 140 | end 141 | 142 | 143 | -- if not _match(iev.name, '%.lua$') then return end 144 | local f = iev.path .. '/' .. iev.name 145 | 146 | -- 修改(新建 create 与 modify 会同时发生) | 改名(新文件名) 147 | -- 这些合并为修改 MOD 事件 148 | if 0 < _band(_IN.MODIFY, iev.mask) or 0 < _band(_IN.MOVED_TO, iev.mask) then 149 | local _, err = ph(f, 'MOD', arg) 150 | if _opts.trace and _is_show then 151 | if err then 152 | tofu.log.e(err) 153 | else 154 | tofu.log.d('reload file:', iev.name) 155 | end 156 | end 157 | 158 | -- 删除 | 改名(旧文件名) 159 | elseif 0 < _band(_IN.MOVE, iev.mask) or 0 < _band(_IN.DELETE, iev.mask) then 160 | ph(f, 'REM') 161 | end 162 | 163 | end 164 | 165 | -- 设置缺省处理 166 | 167 | 168 | 169 | local _M = { _VERSION = '0.1.0' } 170 | 171 | 172 | function _M._install(opts) 173 | -- 只在 worker 上监视 174 | if 'worker' ~= _process.type() then 175 | return 176 | end 177 | 178 | _util.tab_merge(_opts, opts) 179 | for _, p in ipairs(_opts.path) do 180 | if 'table' == type(p) and p[1] then 181 | _plan[#_plan + 1] = {p[1], p[2], p[3]} 182 | if _util.file_exists(p[1]) then 183 | _watcher:add_watch(p[1]) 184 | end 185 | 186 | elseif 'string' == type(p) then 187 | _plan[#_plan + 1] = {p, 'default'} 188 | if _util.file_exists(p) then 189 | _watcher:add_watch(p) 190 | end 191 | 192 | else 193 | if _is_show then 194 | tofu.log.e('watcher path param expect string or table') 195 | end 196 | end 197 | -- if _opts.trace and _is_show then 198 | -- tofu.log.d('jili watch: ', p) 199 | -- end 200 | end 201 | 202 | _watcher:start( _handle ) 203 | return 204 | end 205 | 206 | 207 | return _M 208 | 209 | -------------------------------------------------------------------------------- /lib/resty/tofu/extend/log.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @file log.lua 3 | -- @author d 4 | -- @brief 日志 5 | -- 6 | 7 | 8 | local _M = { _VERSION = '0.1.1' } 9 | 10 | local _fmt = string.format 11 | local _gsub = string.gsub 12 | local _match = string.match 13 | local _rep = string.rep 14 | local _date = os.date 15 | local _concat = table.concat 16 | local _sort = table.sort 17 | 18 | 19 | 20 | local _levels = { 21 | STDERR = ngx.STDERR, 22 | EMERG = ngx.EMERG, 23 | ALERT = ngx.ALERT, 24 | CRIT = ngx.CRIT, 25 | ERR = ngx.ERR, 26 | WARN = ngx.WARN, 27 | NOTICE = ngx.NOTICE, 28 | INFO = ngx.INFO, 29 | DEBUG = ngx.DEBUG, 30 | } 31 | _M.levels = _levels 32 | 33 | local _r_levels = {} 34 | for k, v in pairs(_levels) do 35 | _r_levels[v] = string.lower(k) 36 | end 37 | 38 | 39 | local _colors = { 40 | BLACK = 30, 41 | RED = 31, 42 | GREEN = 32, 43 | YELLOW = 33, 44 | BLUE = 34, 45 | MAGENTA = 35, 46 | CYAN = 36, 47 | WHITE = 37, 48 | } 49 | _M.colors = _colors 50 | 51 | 52 | local _lvl_colors = { 53 | stderr = _colors.RED, 54 | emerg = _colors.MAGENTA, 55 | alert = _colors.BLUE, 56 | crit = _colors.BLUE, 57 | err = _colors.RED, 58 | warn = _colors.YELLOW, 59 | notice = _colors.CYAN, 60 | info = _colors.WHITE, 61 | debug = _colors.GREEN, 62 | } 63 | 64 | 65 | -- -- 66 | -- -- 67 | -- opt { 68 | -- pretty 69 | -- sort 70 | -- } 71 | local function _dump(obj, opt) 72 | if 'table' ~= type(obj) then 73 | return tostring(obj) 74 | end 75 | opt = opt or {level=1} 76 | local pretty = opt.pretty 77 | local level = opt.level or 1 78 | local indent_str = pretty and _rep('\t', level) or '' 79 | local tpl = {} 80 | local keys = {} 81 | for k, _ in pairs(obj) do 82 | keys[#keys + 1] = k 83 | end 84 | if opt.sort then 85 | _sort(keys, function(a, b) return tostring(a) < tostring(b) end) 86 | end 87 | for _, k in ipairs(keys) do 88 | local v = obj[k] 89 | if 'number' ~= type(k) then 90 | k = k .. (pretty and ': ' or ':') 91 | else 92 | k = '' 93 | end 94 | 95 | local vt = type(v) 96 | if 'table' == vt then 97 | opt.level = level + 1 98 | tpl[#tpl + 1] = _fmt('%s%s%s', indent_str, k, _dump(v, opt)) 99 | else 100 | if 'string' == vt then 101 | tpl[#tpl + 1] = _fmt('%s%s"%s"',indent_str, k, v) -- _gsub(v,'\n', '\\n')) 102 | elseif 'function' == vt then 103 | tpl[#tpl + 1] = _fmt('%s%s%s',indent_str, k, 'function') 104 | else 105 | tpl[#tpl + 1] = _fmt('%s%s%s',indent_str, k, tostring(v)) 106 | end 107 | end 108 | 109 | end 110 | 111 | return pretty 112 | and '{\n' .. _concat(tpl, ',\n') .. '\n'.. _rep('\t', level - 1) .. '}' 113 | or '{' .. _concat(tpl, ',') .. '}' 114 | 115 | end 116 | 117 | 118 | 119 | local function _color_fmt(lvl, msg) 120 | local r_lvl = _r_levels[lvl] 121 | return '\27['.._lvl_colors[r_lvl]..'m' .. msg .. '\27[m' 122 | end 123 | 124 | 125 | 126 | local function _log(self, lvl, ...) 127 | if self._level < lvl then 128 | return 129 | end 130 | 131 | 132 | local dump = self._dump 133 | local t = {} 134 | for i=1, select('#', ...) do 135 | t[#t+1] = dump((select(i, ...))) 136 | end 137 | 138 | if self._trace then 139 | local info = debug.getinfo(3) 140 | t[#t + 1] = ' -- ' .. (info.short_src or '') .. ':' .. info.currentline 141 | end 142 | 143 | local r_lvl = _r_levels[lvl] 144 | 145 | local assobj = { 146 | datetime = _date('%Y-%m-%d %H:%M:%S'), 147 | level = r_lvl, 148 | msg = _concat(t, ' '), 149 | } 150 | local assignor = function(v) 151 | return assobj[v] or '' 152 | end 153 | local msg = _gsub(self._fmter, '${%s*(.-)%s*}', assignor) 154 | if self._color then 155 | msg = _color_fmt(lvl, msg) 156 | end 157 | 158 | self._printer(lvl, msg) 159 | end 160 | 161 | 162 | 163 | 164 | 165 | -- 166 | -- 167 | -- 168 | local function _new(opts) 169 | opts = opts or {} 170 | local self = { 171 | _level = opts.level or _levels.DEBUG, 172 | _color = opts.color and true, 173 | _colors = opts.colors or _lvl_colors, 174 | _printer = opts.printer, 175 | _fmter = opts.formater or '${datetime} [${level}] ${msg}\n', 176 | _pretty = opts.pretty, 177 | _trace = opts.trace, 178 | _dump = opts.pretty and function(v) return _dump(v, {pretty=true, sort=true}) end 179 | or _dump 180 | } 181 | 182 | -- --------------------------- 183 | -- make pointfree style 184 | -- 185 | local obj = {} 186 | 187 | function obj.level(lvl) 188 | if lvl and _r_levels[lvl] then 189 | self._level = lvl 190 | end 191 | return self._level 192 | end 193 | 194 | 195 | function obj.d(...) 196 | -- local info = debug.getinfo(2) 197 | -- local fn = info.short_src 198 | -- local line = info.currentline 199 | -- local pre = ' ' .. (fn or '') .. ':' .. line 200 | -- _log(self, _levels.DEBUG, pre, ...) 201 | _log(self, _levels.DEBUG, ...) 202 | end 203 | 204 | function obj.i(...) 205 | _log(self, _levels.INFO, ...) 206 | end 207 | 208 | function obj.n(...) 209 | _log(self, _levels.NOTICE, ...) 210 | end 211 | 212 | function obj.w(...) 213 | _log(self, _levels.WARN, ...) 214 | end 215 | 216 | function obj.e(...) 217 | _log(self, _levels.ERR, ...) 218 | end 219 | 220 | return obj 221 | 222 | end 223 | 224 | 225 | 226 | -- 227 | -- 228 | -- 229 | function _M.ngxlog(opts) 230 | opts = opts or {} 231 | local el = require 'ngx.errlog' 232 | local raw_log = el.raw_log 233 | local function printer() 234 | el.raw_log = function (lvl, msg) 235 | raw_log(lvl, _color_fmt(lvl, msg)) 236 | end 237 | return el.raw_log 238 | end 239 | 240 | return _new({ 241 | level = opts.level or _levels.DEBUG, 242 | formater = '${msg}', 243 | printer = opts.printer or printer(), 244 | color = opts.color, 245 | pretty = opts.pretty, 246 | trace = opts.trace, 247 | }) 248 | end 249 | 250 | -- 251 | -- 252 | -- 253 | function _M.console(opts) 254 | opts = opts or {} 255 | local function printer() 256 | local fd = io.open('/dev/stdout', 'ab') 257 | fd:setvbuf('no') 258 | return function (lvl, msg) 259 | fd:write(msg) 260 | end 261 | end 262 | 263 | return _new({ 264 | level = opts.level or _levels.DEBUG, 265 | formater = opts.formater or '${datetime} [${level}] ${msg}\n', 266 | printer = opts.printer or printer(), 267 | color = opts.color, 268 | pretty = opts.pretty, 269 | trace = opts.trace, 270 | }) 271 | end 272 | 273 | 274 | -- 275 | -- 276 | -- 277 | function _M.file(opts) 278 | local function printer() 279 | local rotate = opts.rotate 280 | local file = opts.file 281 | local prefix, suffix 282 | if rotate then 283 | prefix, suffix = _match(opts.file, '^(.+)%.(.-)$') 284 | prefix = prefix or opts.file 285 | suffix = suffix or '' 286 | end 287 | local fd = nil 288 | return function (lvl, msg) 289 | if 'day' == rotate and file then 290 | local day = os.date('%Y-%m-%d') 291 | local cur_file = prefix .. '.' .. day .. '.' .. suffix 292 | if file ~= cur_file then 293 | file = cur_file 294 | if fd then 295 | fd:close() 296 | fd = nil 297 | end 298 | end 299 | end 300 | 301 | if not fd then 302 | fd = io.open(file or '/dev/stdout', 'ab') 303 | fd:setvbuf('no') 304 | end 305 | fd:write(msg) 306 | end 307 | end 308 | 309 | return _new({ 310 | level = opts.level or _levels.INFO, 311 | formater = opts.formater or '${datetime} [${level}] ${msg}\n', 312 | printer = opts.printer or printer(), 313 | pretty = opts.pretty, 314 | color = opts.color, 315 | trace = opts.trace, 316 | }) 317 | end 318 | 319 | 320 | 321 | return _M 322 | 323 | -------------------------------------------------------------------------------- /lib/resty/tofu/extend/model.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @file mode.lua 3 | -- @author d 4 | -- @version 0.1.0 5 | -- 6 | 7 | local _base = require 'resty.tofu.model-mysql-helper' 8 | local _refile = require 'resty.tofu.refile' 9 | local _util = require 'resty.tofu.util' 10 | local _merge = _util.tab_merge 11 | local _func = _util.functional 12 | 13 | 14 | local _default = { 15 | -- path = 'unix socket' 16 | host = '127.0.0.1', 17 | port = 3306, 18 | database = 'mysql_db_name', 19 | prefix = '', 20 | user = 'root', 21 | password = '', 22 | charset = 'utf8', 23 | timeout = 5 * 1000, 24 | max_packet_size = 2 * 1024 * 1024, 25 | pool_size = 64, 26 | pool_idle_timeout = 900 * 1000, 27 | 28 | logger = tofu and tofu.log.n or print, 29 | } 30 | 31 | 32 | -- 33 | -- 34 | -- 35 | local _model_root = tofu.APP_PATH .. 'model/' 36 | 37 | 38 | -- 39 | -- 40 | -- 41 | local function _getmodel(name) 42 | if not name then return end 43 | local f = _model_root .. name .. '.lua' 44 | return _refile.load(f) 45 | end 46 | 47 | 48 | 49 | 50 | -- 51 | -- 52 | -- 53 | return function(options) 54 | 55 | -- 56 | -- @param name 表名 | model/.lua 57 | -- @run_opts 配置 | model 58 | -- 59 | local function _model(name, run_opts) 60 | local model, err = _getmodel(name) or {} 61 | local obj = _merge({}, _default, model) 62 | 63 | run_opts = run_opts or options.default 64 | if 'string' == type(run_opts) then 65 | run_opts = options[run_opts] 66 | end 67 | 68 | local ot = type(run_opts) 69 | if 'table' == ot then 70 | _merge(obj, run_opts[0] or run_opts) -- 兼容函数化的run_opts 71 | elseif 'function' == ot then 72 | obj._parser = run_opts 73 | else 74 | error('model invalid argment #2') 75 | end 76 | 77 | -- 表名 78 | obj.name = obj.prefix .. (name or '') 79 | 80 | -- 重写 x.model()方法, 继成当前配置(状态, 如链接等...) 81 | obj.model = function(_, name, new_opts) 82 | new_opts = _merge({}, obj, new_opts) 83 | return _model(name, new_opts) 84 | end 85 | 86 | setmetatable(obj, {__index = _base}) 87 | -- 函数化table,仅是为了支持链式 88 | return _func(obj) 89 | end 90 | 91 | return _model 92 | 93 | end 94 | 95 | 96 | -------------------------------------------------------------------------------- /lib/resty/tofu/extend/session.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @author d 3 | -- @version 0.1.0 4 | -- @brief session 5 | -- @see https://github.com/bungle/lua-resty-session 6 | -- 7 | 8 | 9 | local _util = require 'resty.tofu.util' 10 | local _tab_merge = _util.tab_merge 11 | 12 | 13 | 14 | local _options = { 15 | name = 'tofu_session', 16 | ttl = 20 * 60, 17 | renewal = true, 18 | -- storage = 'shm', -- | '' 19 | storage = 'cookie', -- | '' 20 | secret = '5df7f1701b778d03d57456afea567922', 21 | } 22 | 23 | 24 | 25 | local _M = {} 26 | 27 | 28 | -- 29 | -- 因为resty.session 无法在 init_worker_by_lua_* 阶段加载, 这里做一个hack 30 | -- 使用ngx.ctx 保存ses的引用 31 | -- 32 | local _ses = nil 33 | local _get_ses = function (lock) 34 | if not _ses then 35 | _ses = require 'resty.session' 36 | end 37 | 38 | if not ngx.ctx.__tofu_session then 39 | local opts = _tab_merge({}, _options) 40 | local ses = _ses.open(opts) 41 | if lock and not ses.started then 42 | ses:start() 43 | end 44 | ngx.ctx.__tofu_session = ses 45 | end 46 | return ngx.ctx.__tofu_session 47 | end 48 | 49 | -- 50 | -- 适配 tofu expend 接口 51 | -- 52 | function _M._install(opts) 53 | _tab_merge(_options, opts) 54 | _options.cookie = {lifetime = _options.ttl} 55 | return _M 56 | end 57 | 58 | 59 | -- --------------------------------- 60 | -- api 61 | -- 62 | 63 | -- 64 | -- 65 | -- 66 | function _M.get(key) 67 | local ses = _get_ses() 68 | local v = ses.data[key] 69 | if ses.expires and false ~= _options.renewal then 70 | ses:save() 71 | end 72 | ses:close() 73 | return v 74 | end 75 | 76 | 77 | 78 | 79 | -- 80 | -- 81 | -- 82 | function _M.set(key, value) 83 | if not key then 84 | return nil 85 | end 86 | 87 | local ses = _get_ses(true) 88 | local old = ses.data[key] 89 | ses.data[key] = value 90 | ses:save() 91 | return old 92 | end 93 | 94 | 95 | 96 | 97 | -- 98 | -- 删除相关session 99 | -- 100 | function _M.destroy() 101 | _get_ses():destroy() 102 | end 103 | 104 | 105 | return _M 106 | 107 | -------------------------------------------------------------------------------- /lib/resty/tofu/extend/task.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @author d 3 | -- @version 0.1.1 4 | -- @brief 定时任务 5 | -- 6 | 7 | 8 | 9 | local _process = require 'ngx.process' 10 | local _crontab = require 'resty.tofu.crontab' 11 | local _config = require 'resty.tofu.extend.config' 12 | local _util = require 'resty.tofu.util' 13 | 14 | 15 | local _M = { _VERSION = '0.1.1' } 16 | 17 | 18 | local _default = { 19 | worker = 'privileged agent', -- 使用特权进程或指定worker进程执行 20 | task = 'task', -- string | { task = {}} 21 | } 22 | 23 | -- task 24 | -- { 25 | -- after 26 | -- every 27 | -- cron 28 | -- enable 29 | -- handle 30 | -- immediate 31 | -- } 32 | -- 33 | 34 | local _log = tofu and tofu.log.e or error 35 | 36 | 37 | function _M._install(opts) 38 | opts = _util.tab_merge(opts, _default) 39 | 40 | if _process.type() ~= opts.worker then 41 | if ngx.worker.id() ~= opts.worker then 42 | return 43 | end 44 | end 45 | 46 | if 'string' == type(opts.task) then 47 | local config = tofu.config or 48 | _config._install({ env = tofu.env, prefix = tofu.ROOT_PATH .. 'conf/'}) 49 | opts.task = config.task 50 | end 51 | 52 | local cur_env = getfenv() 53 | for _, t in ipairs(opts.task) do 54 | if false ~= t.enable then 55 | local h = t.handle 56 | 57 | if 'string' == type(h) then 58 | h = require(t.handle) 59 | end 60 | 61 | if not _util.iscallable(h) then 62 | local cnf = '\n' .. _util.tab_str(t, {pretty = true}) 63 | return _log('task config error: [handle] must callable' .. cnf) 64 | end 65 | 66 | if 'function' == type(h) then 67 | setfenv(h, cur_env) 68 | end 69 | 70 | if t.after then 71 | if 'number' ~= type(t.after) or t.after < 0 then 72 | local cnf = '\n' .. _util.tab_str(t, {pretty = true}) 73 | return _log('task config error: [after] must be a positive number' .. cnf) 74 | end 75 | _crontab.add_after(t.after, h) 76 | 77 | elseif t.interval then 78 | if 'number' ~= type(t.interval) or t.interval < 1 then 79 | local cnf = '\n' .. _util.tab_str(t, {pretty = true}) 80 | return _log('task config error: [interval] must be a positive number(>0)' .. cnf) 81 | end 82 | _crontab.add_every(t.interval, h) 83 | 84 | elseif t.cron then 85 | if 'string' ~= type(t.cron) then 86 | local cnf = '\n' .. _util.tab_str(t, {pretty = true}) 87 | return _log('task config error: [cron] must be a crontab format string' .. cnf) 88 | end 89 | _crontab.add_cron(t.cron, h) 90 | end 91 | 92 | -- 立即执行一次 93 | if true == t.immediate then 94 | ngx.timer.at(0, h) 95 | end 96 | 97 | end 98 | end -- end for 99 | 100 | _crontab.start() 101 | 102 | end 103 | 104 | 105 | return _M 106 | -------------------------------------------------------------------------------- /lib/resty/tofu/extend/view.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @file view.lua 3 | -- @author d 4 | -- @version 0.1.2 5 | -- 6 | 7 | 8 | local _str_sub = string.sub 9 | local _tab_merge = require 'resty.tofu.util'.tab_merge 10 | local _ctx = require 'resty.tofu.ctx' 11 | local _template = require 'resty.template' 12 | 13 | 14 | 15 | -- 16 | -- 配置 17 | -- 18 | local _options = { 19 | template_root = 'view/', -- 模版搜索路径 20 | extname = '.html', -- 模板后缀 21 | cache = false, -- 缓存(建议开发或低内存限制时禁用) 22 | } 23 | 24 | 25 | local _ext_len = #_options.extname 26 | 27 | local function _template_init() 28 | -- fix template_root path 29 | if '/' ~= _options.template_root:sub(-1) then 30 | _options.template_root = _options.template_root .. '/' 31 | end 32 | 33 | _ext_len = #_options.extname 34 | _template.root = _options.template_root 35 | _template.caching(_options.cache) 36 | 37 | end 38 | 39 | 40 | 41 | 42 | 43 | 44 | local function _getassign() 45 | local ctx = _ctx.fetch() 46 | if not ctx.assign then 47 | ctx.assign = {} 48 | end 49 | return ctx.assign 50 | end 51 | 52 | 53 | local _M = {} 54 | 55 | 56 | function _M.assign(param1, param2) 57 | local assign = _getassign() 58 | local pt = type (param1) 59 | if 'table' == pt then 60 | _tab_merge(assign, param1) 61 | elseif 'string' == pt then 62 | assign[param1] = param2 63 | else 64 | error ('param 1 error') 65 | end 66 | return _M 67 | end 68 | 69 | 70 | function _M.render(tpl, param) 71 | local ctx = _ctx.fetch() 72 | tpl = tpl or (ctx.handler .. '_' .. ctx.action .. _options.extname) 73 | if _str_sub(tpl, -_ext_len) ~= _options.extname then 74 | tpl = tpl .. _options.extname 75 | end 76 | return _template.process(tpl, param) 77 | end 78 | 79 | 80 | function _M.display(tpl, param) 81 | local assign = _getassign() 82 | param = _tab_merge({}, assign, param) 83 | local content = _M.render(tpl, param) 84 | if content then 85 | ngx.print(content) 86 | end 87 | return false 88 | end 89 | 90 | 91 | 92 | return function(options) 93 | _tab_merge(_options, optins) 94 | _template_init() 95 | return _M 96 | end 97 | 98 | -------------------------------------------------------------------------------- /lib/resty/tofu/inotify.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @file inotify.lua 3 | -- @author d 4 | -- @brif 文件监测 5 | -- 依赖: inotify 库 6 | -- 7 | -- 8 | -- example -- 9 | -- 10 | -- local w = _M.new(opts) 11 | -- w:add_watch('.') 12 | -- w:start( function (iev) 13 | -- print ('mask:', iev.mask, ' # ',iev.path ..'/'.. iev.name) 14 | -- end) 15 | 16 | 17 | -- ----------------------------------------------- 18 | -- @param opts { 19 | -- buf_size : 4096 byte 20 | -- timeout : 0.23 s 21 | -- } 22 | -- 23 | -- @param iev (inotify_event) { 24 | -- isdir 25 | -- mask 26 | -- path 27 | -- name 28 | -- } 29 | 30 | 31 | 32 | 33 | 34 | local _bit = require 'bit' 35 | local _ffi = require 'ffi' 36 | local _c = _ffi.C 37 | 38 | 39 | _ffi.cdef [[ 40 | 41 | int inotify_init(void); 42 | int inotify_init1(int __flags); 43 | int inotify_add_watch(int fd, const char *pathname, uint32_t mask); 44 | int inotify_rm_watch(int __fd, int __wd); 45 | 46 | int read(int fd, void *buf, size_t count); 47 | 48 | 49 | struct inotify_event { 50 | int wd; 51 | uint32_t mask; 52 | uint32_t cookie; 53 | uint32_t len; 54 | char name []; 55 | }; 56 | 57 | 58 | ]] 59 | 60 | 61 | 62 | local _IN = { 63 | ACCESS = 0x00000001, 64 | MODIFY = 0x00000002, 65 | ATTRIB = 0x00000004, 66 | CLOSE_WRITE = 0x00000008, 67 | CLOSE_NOWRITE = 0x00000010, 68 | CLOSE = _bit.bor(0x8, 0x10), 69 | OPEN = 0x00000020, 70 | MOVED_FROM = 0x00000040, 71 | MOVED_TO = 0x00000080, 72 | MOVE = _bit.bor(0x40, 0x80), -- MOVED_FROM | MOVED_TO 73 | CREATE = 0x00000100, 74 | DELETE = 0x00000200, 75 | DELETE_SELF = 0x00000400, 76 | MOVE_SELF = 0x00000800, 77 | 78 | UNMOUNT = 0x00002000, 79 | Q_OVERFLOW = 0x00004000, 80 | IGNORED = 0x00008000, 81 | 82 | 83 | 84 | ISDIR = 0x40000000, 85 | 86 | NONBLOCK = 0x00000800, -- 00004000 87 | CLOEXEC = 0x00080000, -- 02000000 88 | } 89 | 90 | _IN.ALL_EVENTS = _bit.bor( 91 | _IN.ACCESS , 92 | _IN.MODIFY , 93 | _IN.ATTRIB , 94 | _IN.CLOSE_WRITE , 95 | _IN.CLOSE_NOWRITE , 96 | _IN.CLOSE , 97 | _IN.OPEN , 98 | _IN.MOVED_FROM , 99 | _IN.MOVED_TO , 100 | _IN.MOVE , 101 | _IN.CREATE , 102 | _IN.DELETE , 103 | _IN.DELETE_SELF , 104 | _IN.MOVE_SELF 105 | ) 106 | 107 | 108 | 109 | local _IN_MAP = {} 110 | for k, v in pairs(_IN) do 111 | _IN_MAP[v] = k 112 | end 113 | 114 | 115 | local _EVT_MASK = _bit.bor( 116 | _IN.MODIFY, 117 | _IN.CREATE, 118 | _IN.DELETE, 119 | _IN.MOVE 120 | ) 121 | 122 | 123 | 124 | 125 | 126 | local _M = { _VERSION = '0.1.1' } 127 | _M.IN = _IN 128 | _M.IN_MAP = _IN_MAP 129 | 130 | 131 | 132 | 133 | -- 134 | -- @param opts { 135 | -- buf_size : 4096 byte 136 | -- timeout : 0.23 s 137 | -- } 138 | -- 139 | function _M.new(opts) 140 | opts = opts or {} 141 | local obj = { 142 | _buf_size = opts.buf_size or 4096, 143 | _timeout = 0.23, 144 | _wds = {}, 145 | } 146 | obj._fd = _c.inotify_init1(0x80800) -- NONBLOCK | CLOEXEC 147 | return setmetatable(obj, { __index = _M }) 148 | end 149 | 150 | 151 | 152 | 153 | -- 154 | -- @param dir 目录 155 | -- 156 | function _M:add_watch(dir) 157 | local cmd = 'find ' .. dir .. ' -type d' 158 | local f = io.popen(cmd) 159 | for line in f:lines() do 160 | local wd = _c.inotify_add_watch(self._fd, line, _EVT_MASK) 161 | if 0 < wd then 162 | self._wds[wd] = line 163 | end 164 | end 165 | end 166 | 167 | 168 | 169 | 170 | -- 171 | -- 172 | -- 173 | function _M:rm_watch(wd) 174 | self._wds[wd] = nil 175 | _c.inotify_rm_watch(self._fd, wd) 176 | end 177 | 178 | 179 | 180 | 181 | -- 182 | -- 183 | -- 184 | function _M:stop() 185 | self._iswork = nil 186 | end 187 | 188 | 189 | 190 | 191 | -- 192 | -- 193 | -- 194 | function _M:start(handler) 195 | if self._iswork then return end 196 | self._iswork = true 197 | local iev = _ffi.new('struct inotify_event') 198 | local iev_size = _ffi.sizeof(iev) 199 | local buf = _ffi.new('uint8_t[?]', self._buf_size) 200 | local ret = 0 201 | 202 | -- -- 203 | -- while self._iswork do 204 | -- ret = _c.read(self._fd, buf, self._buf_size) 205 | -- if 0 < ret then 206 | -- local i = 0 207 | -- while i < ret do 208 | -- local iev = _ffi.cast('struct inotify_event *', buf + i) 209 | -- i = i + iev_size + iev.len 210 | -- local info = { 211 | -- isdir = 0 < _bit.band(iev.mask, _IN.ISDIR), 212 | -- mask = _bit.band(iev.mask, 0xffff) or iev.mask, 213 | -- path = self._wds[iev.wd] or '.', 214 | -- name = _ffi.string(iev.name, iev.len) 215 | -- } 216 | 217 | -- -- pass 218 | -- if _IN.IGNORED == info.mask then 219 | 220 | -- -- 221 | -- elseif info.isdir and _IN.CREATE == info.mask then 222 | -- self:add_watch(info.path .. '/' .. info.name) 223 | 224 | -- -- 225 | -- elseif info.isdir and _IN.DELETE_SELF == info.mask then 226 | -- self:rm_watch(iev.wd) 227 | 228 | -- -- 229 | -- elseif handler then 230 | -- handler(info) 231 | -- end 232 | -- end 233 | -- end 234 | -- ngx.sleep(self._timeout) 235 | -- end 236 | 237 | local function loop() 238 | if not self._iswork then return end 239 | ret = _c.read(self._fd, buf, self._buf_size) 240 | if 0 < ret then 241 | local i = 0 242 | while i < ret do 243 | local iev = _ffi.cast('struct inotify_event *', buf + i) 244 | i = i + iev_size + iev.len 245 | local info = { 246 | isdir = 0 < _bit.band(iev.mask, _IN.ISDIR), 247 | mask = _bit.band(iev.mask, 0xffff) or iev.mask, 248 | path = self._wds[iev.wd] or '.', 249 | name = _ffi.string(iev.name) 250 | } 251 | 252 | -- pass 253 | if _IN.IGNORED == info.mask then 254 | 255 | -- 256 | elseif info.isdir and _IN.CREATE == info.mask then 257 | self:add_watch(info.path .. '/' .. info.name) 258 | 259 | -- 260 | elseif info.isdir and _IN.DELETE_SELF == info.mask then 261 | self:rm_watch(iev.wd) 262 | 263 | -- 264 | elseif handler then 265 | handler(info) 266 | end 267 | end 268 | end 269 | ngx.timer.at(self._timeout, loop) 270 | end 271 | 272 | loop() 273 | 274 | end 275 | 276 | 277 | 278 | return _M 279 | 280 | 281 | 282 | 283 | 284 | -------------------------------------------------------------------------------- /lib/resty/tofu/mate.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @file mate.lua 3 | -- @author d 4 | -- @version 0.1.3 5 | -- 6 | -- 7 | 8 | 9 | return { 10 | _NAME = 'tofu', 11 | _VERSION = '0.1.x.alpha', 12 | _SERVER_TOKENS = 'tofu 0.1.x.alpha', 13 | core = {}, 14 | tofu = {}, 15 | } 16 | 17 | -------------------------------------------------------------------------------- /lib/resty/tofu/middleware/controller.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- tofu controller 中间件 3 | -- @file controller.lua 4 | -- @author d 5 | -- @version 0.1.1 6 | -- 7 | 8 | 9 | local _sub = string.sub 10 | 11 | local _refile = require 'resty.tofu.refile' 12 | local _tab_merge = require 'resty.tofu.util'.tab_merge 13 | local _ctx = require 'resty.tofu.ctx' 14 | 15 | 16 | 17 | 18 | local _default = { 19 | suffix = '', -- _action 后缀如 _action 20 | logger = tofu and tofu.log.e or print, -- function 21 | controller_root = tofu.APP_PATH .. 'controller/', 22 | controller_allow_underscore = false, -- 是否允许下划线开头的controller 23 | action_allow_underscore = false, -- 是否允许下划线开头的action 24 | } 25 | 26 | 27 | -- 28 | -- 29 | -- 30 | return function(options) 31 | options = _tab_merge(options, _default) 32 | 33 | local _suffix = options.suffix 34 | local _controller_root = options.controller_root 35 | local _controller_allow_underscore = options.controller_allow_underscore 36 | local _action_allow_underscore = options.action_allow_underscore 37 | local _log = options.logger 38 | 39 | 40 | local function _gethandler(p) 41 | local f = _controller_root .. p .. '.lua' 42 | local h, err = _refile.load(f) 43 | if err then 44 | error(err) 45 | end 46 | return h 47 | end 48 | 49 | return function (ctx, flow) 50 | if not ctx.controller then 51 | ngx.status = ngx.HTTP_NOT_FOUND 52 | return 53 | end 54 | 55 | -- 56 | -- 57 | -- 58 | if not _controller_allow_underscore and '_' == _sub(ctx.controller, 1,1) then 59 | ngx.status = ngx.HTTP_NOT_FOUND 60 | return 61 | end 62 | if _action_allow_underscore and '_' == _sub(ctx.action or '', 1,1) then 63 | ngx.status = ngx.HTTP_NOT_FOUND 64 | return 65 | end 66 | 67 | 68 | local handler = _gethandler(ctx.handler) 69 | if not handler then 70 | _log('controller must return table') 71 | ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR 72 | return 73 | end 74 | 75 | -- enter event 76 | local fn_enter = handler['_enter'] 77 | if 'function' == type(fn_enter) and false == fn_enter(ctx.args) then 78 | return 79 | end 80 | 81 | -- action 82 | local fn = handler[ctx.action .. _suffix] 83 | if not fn then 84 | ngx.status = ngx.HTTP_NOT_FOUND 85 | _log('controller[', ctx.handler, '] action [', ctx.action, '] not exists') 86 | return 87 | end 88 | if false == fn(ctx.args) then 89 | return 90 | end 91 | 92 | -- leave event 93 | local fn_leave = handler['_leave'] 94 | if 'function' == type(fn_leave) and false == fn_leave(ctx.args) then 95 | return 96 | end 97 | 98 | -- next 99 | flow() 100 | end 101 | 102 | 103 | end -- end middleware 104 | 105 | -------------------------------------------------------------------------------- /lib/resty/tofu/middleware/guard.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- tofu guard 参数过滤中间件 3 | -- @file guard.lua 4 | -- @author d 5 | -- @version 0.1.1 6 | -- 7 | 8 | 9 | local _refile = require 'resty.tofu.refile' 10 | local _ctx = require 'resty.tofu.ctx' 11 | local _log = tofu.log 12 | local _sub = string.sub 13 | 14 | 15 | -- 16 | -- 17 | -- 18 | -- 19 | return function(options) 20 | options = options or {} 21 | 22 | -- default 23 | local _guard_root = options.guard_root or tofu.APP_PATH .. 'guard/' 24 | local _suffix = options.suffix or '' 25 | 26 | 27 | local function _gethandler(p) 28 | if not p then return nil end 29 | local f = _guard_root .. p .. '.lua' 30 | local h, err = _refile.load(f, false) 31 | if err and 'cannot open' ~= _sub(err, 1, 11)then 32 | tofu.log.w(err) 33 | end 34 | return h 35 | end 36 | 37 | 38 | return function (ctx, flow) 39 | local handler = _gethandler(ctx.handler) 40 | if not handler then 41 | return flow() 42 | end 43 | 44 | -- enter 45 | local fn_enter = handler['_enter'] 46 | if 'function' == type(fn_enter) 47 | and false == fn_enter(ctx.args) then 48 | return 49 | end 50 | 51 | -- validate 52 | local fn = handler[ctx.action .. _suffix] 53 | if fn and false == fn(ctx.args) then 54 | return 55 | end 56 | 57 | flow() 58 | end 59 | 60 | 61 | end -- end middleware 62 | 63 | -------------------------------------------------------------------------------- /lib/resty/tofu/middleware/payload.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- tofu payload 参数预处理中间件 3 | -- @file payload.lua 4 | -- @author d 5 | -- @version 0.1.1 6 | -- 7 | 8 | local _concat = table.concat 9 | local _match = string.match 10 | local _sub = string.sub 11 | local _find = string.find 12 | 13 | local _upload = require 'resty.upload' 14 | local _json = require 'cjson.safe' 15 | local _util = require 'resty.tofu.util' 16 | local _tab_merge = _util.tab_merge 17 | local _isempty = _util.isempty 18 | 19 | 20 | -- 21 | -- 解析器 function -> {get={}, post={}, file={}} 22 | -- 23 | local _parser = { 24 | 25 | ['application/x-www-form-urlencoded'] = function() 26 | ngx.req.read_body() 27 | return { post=ngx.req.get_post_args() } 28 | end, 29 | 30 | 31 | 32 | ['application/json'] = function() 33 | ngx.req.read_body() 34 | local data = ngx.req.get_body_data() 35 | return { post = (_json.decode(data)) } 36 | end, 37 | 38 | -- 39 | -- 40 | -- @return { 41 | -- post = {}, 42 | -- file = { 43 | -- = { 44 | -- name, 45 | -- type, 46 | -- data, 47 | -- size 48 | -- } 49 | -- } 50 | -- } 51 | -- 52 | ['multipart/form-data'] = function() 53 | local form, err = _upload:new() 54 | if not form then 55 | return 56 | end 57 | form:set_timeout(1000) 58 | -- 参数存放 59 | local post, file = {}, {} 60 | local vals, name, filename, filetype, datasize 61 | repeat 62 | local typ, res, err = form:read() 63 | if not typ then 64 | return 65 | end 66 | if 'header' == typ then 67 | vals = {} 68 | datasize = 0 69 | local cd = res[#res] 70 | name = _match(cd, 'name="(.-)"') 71 | filename = _match(cd, 'filename="(.-)"') 72 | if filename then 73 | local _, res, err = form:read() 74 | filetype = res[2] 75 | end 76 | 77 | elseif 'body' == typ then 78 | vals[#vals + 1] = res 79 | if filename then 80 | datasize = datasize + #res 81 | end 82 | 83 | elseif 'part_end' == typ then 84 | if name then 85 | if filename and not _isempty(vals) then 86 | file[name] = { 87 | name = filename, 88 | type = filetype, 89 | data = _concat(vals), 90 | size = datasize, 91 | } 92 | else 93 | post[name] = _concat(vals) 94 | end 95 | end 96 | 97 | end 98 | 99 | until 'eof' == typ 100 | 101 | return {post = post, file = file} 102 | 103 | end 104 | } 105 | 106 | 107 | -- 108 | -- 参数元表:用于支持tofu.args接口实现: args.get/post/file[key] | args('key') 109 | -- 110 | local _args_mt = { 111 | __call = function (t, k) 112 | return rawget(t.post, k) 113 | or rawget(t.file, k) 114 | or rawget(t.get, k) 115 | or rawget(t, k) 116 | end 117 | } 118 | 119 | 120 | local function _payload(ctx, flow) 121 | if not ctx.args then 122 | ctx.args = {} 123 | end 124 | local args = ctx.args 125 | _tab_merge(args, { get = {}, post={}, file={} }) 126 | 127 | -- get 参数 128 | local uri_args, err = ngx.req.get_uri_args() 129 | _tab_merge(args, { get = uri_args }) 130 | 131 | -- 非get, 非options 请求, post | put ... 132 | local method = ngx.req.get_method() 133 | if 'GET' ~= method and 'OPTIONS' ~= method then 134 | local content_type = (ngx.req.get_headers()['content-type'] or '') 135 | local isep = _find(content_type, ';', 1, true) 136 | if isep then 137 | content_type = _sub(content_type, 1, isep - 1) 138 | end 139 | local handle = _parser[ content_type ] 140 | if handle then 141 | _tab_merge(args, handle() or {}) 142 | end 143 | end 144 | 145 | setmetatable(args, _args_mt) 146 | flow() 147 | end 148 | 149 | 150 | -- module 151 | return function (options) 152 | options = options or {} 153 | _tab_merge(_parser, options.parser) 154 | return _payload 155 | end 156 | 157 | -------------------------------------------------------------------------------- /lib/resty/tofu/middleware/router.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- tofu router 路由中间件 3 | -- @file router.lua 4 | -- @author d 5 | -- @version 0.1.1 6 | -- 7 | 8 | local _concat = table.concat 9 | local _insert = table.insert 10 | 11 | local _util = require 'resty.tofu.util' 12 | local _split = _util.msplit 13 | local _tab_merge = _util.tab_merge 14 | local _file_exists = _util.file_exists 15 | 16 | -- local _log = tofu.log 17 | local _PATH = tofu.APP_PATH .. 'controller/' 18 | 19 | 20 | 21 | local _cache = {} 22 | local function _rule_exists(p) 23 | local ok = _cache[p] 24 | if not ok then 25 | ok = _file_exists(p) 26 | if ok then 27 | _cache[p] = true 28 | end 29 | end 30 | return ok 31 | end 32 | 33 | local function _rule_cache(uri, r) 34 | if r then 35 | _cache[uri] = r 36 | return r 37 | else 38 | return _cache[uri] 39 | end 40 | end 41 | 42 | 43 | -- 44 | -- @param uri /module/[.../../]controller/action[/arg1/arg2.../...] 45 | -- 46 | local function _rule_default(uri, options) 47 | local r = _rule_cache(uri) 48 | if r then 49 | return r 50 | end 51 | 52 | local resolve = _split(uri, '/') 53 | local flag, len = 1, #resolve 54 | local p = '' -- _PATH 55 | local m = options.module 56 | local c 57 | local a 58 | local args = {} 59 | 60 | -- find module 61 | if 0 < len and _rule_exists(_PATH .. resolve[flag]) then 62 | m = resolve[flag] 63 | flag = flag + 1 64 | -- check default module 65 | elseif not _rule_exists(_PATH .. m) then 66 | return nil, 'router: module not exists' 67 | end 68 | p = m 69 | 70 | -- find controller 71 | while flag <= len do 72 | p = p ..'/'.. resolve[flag] 73 | if (_rule_exists(_PATH .. p .. '.lua')) then 74 | c = resolve[flag] 75 | break 76 | end 77 | flag = flag + 1 78 | end 79 | if not c then 80 | if not _rule_exists(_PATH .. p .. '/'.. options.controller .. '.lua') then 81 | return nil, 'router: controller not exists' 82 | else 83 | c = options.controller 84 | p = p .. '/' .. c 85 | end 86 | end 87 | 88 | flag = flag + 1 89 | -- find action 90 | a = resolve[flag] or options.action 91 | 92 | -- find args 93 | for i = flag + 1, len do 94 | _insert(args, resolve[i]) 95 | end 96 | 97 | r = { 98 | handler = p, 99 | module = m, 100 | controller = c, 101 | action = a, 102 | args = args 103 | } 104 | 105 | if #args < 1 then 106 | _rule_cache(uri, r) 107 | end 108 | 109 | return r 110 | end 111 | 112 | 113 | 114 | 115 | -- 116 | -- 117 | -- 118 | return function (options) 119 | -- default 120 | local _options = { 121 | module = 'default', 122 | controller = 'index', 123 | action = 'index', 124 | } 125 | 126 | _tab_merge(_options, options) 127 | 128 | return function (ctx, flow) 129 | local r, err = _rule_default(ngx.var.uri, _options) 130 | if not r then 131 | ngx.status = ngx.HTTP_NOT_FOUND 132 | return nil, err 133 | end 134 | -- ctx.route = r 135 | _tab_merge(ctx, r) 136 | flow() 137 | end 138 | 139 | end -- end middleware 140 | 141 | 142 | -------------------------------------------------------------------------------- /lib/resty/tofu/middleware/trace.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- tofu trace 中间件 3 | -- @file trace.lua 4 | -- @author d 5 | -- @version 0.1.0 6 | -- 7 | 8 | local _clock = require 'resty.tofu.util'.getusec 9 | local _fmt = string.format 10 | 11 | 12 | 13 | 14 | local _logger = function(msg) 15 | ngx.log(ngx.INFO, msg) 16 | end 17 | 18 | 19 | local function trace(ctx, flow) 20 | local st = _clock() 21 | flow() 22 | _logger( 23 | ngx.req.get_method(), 24 | ngx.var.request_uri, 25 | ngx.status, 26 | _fmt('%.2fms', (_clock() - st) / 1000) 27 | ) 28 | end 29 | 30 | 31 | -- 32 | -- @param options { 33 | -- logger 34 | -- } 35 | -- 36 | return function (options) 37 | options = options or {} 38 | _logger = options.logger or _logger 39 | return trace 40 | end 41 | -------------------------------------------------------------------------------- /lib/resty/tofu/model-mysql-helper.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @author d 3 | -- @version 0.1.1 4 | -- @brief mysql访问助手 5 | -- 6 | 7 | local _format = string.format 8 | local _gsub = string.gsub 9 | local _lower = string.lower 10 | local _match = string.match 11 | local _concat = table.concat 12 | 13 | local ok, _mysql = pcall(require, 'resty.mysql') 14 | if not ok then error('resty.mysql module required') end 15 | 16 | local _util = require 'resty.tofu.util' 17 | local _merge = _util.tab_merge 18 | local _isempty = _util.isempty 19 | local _functional = _util.functional 20 | local _tabnew = _util.tab_new 21 | local _clock = _util.getusec 22 | 23 | -- json数组标记 24 | local _json_array_mt = require 'cjson' .empty_array_mt 25 | 26 | 27 | -- 28 | -- 数据过虑器 29 | -- 30 | local _nil = function() return 'NULL' end 31 | local _boo = function(b) return tostring(b) end 32 | local _num = function (num) return tostring(tonumber(num or 0)) end 33 | local _str = ngx.quote_sql_str 34 | 35 | -- 有特殊不转换处理 36 | -- {'xxxx'} 37 | local _tab = function (obj) return 'string' == type(obj[1]) and obj[1] or '' end 38 | local _use = function (obj) return ngx.null == obj and 'NULL' or '' end 39 | 40 | -- 不支持的类型 41 | -- function 42 | -- thread 43 | local _non = function (non) error('sql not support ' .. type(non) .. ' type') end 44 | 45 | 46 | -- 47 | -- 48 | -- 49 | local _var_map = { 50 | ['nil'] = _nil, 51 | ['boolean'] = _boo, 52 | ['number'] = _num, 53 | ['string'] = _str, 54 | ['table'] = _tab, 55 | ['userdata'] = _use, 56 | } 57 | 58 | -- 自动识别过虑处理函数 59 | local _var = function (val) 60 | local typ = type (val) 61 | local vf = _var_map[typ] or _non 62 | return vf(val) 63 | end 64 | 65 | 66 | 67 | -- 68 | -- 扩展操作符处理 69 | -- 70 | local _ext_operator = function (k, v) 71 | -- 一元 72 | if 1 == #v then 73 | return _format('`%s` %s', k, v[1] or '') 74 | end 75 | 76 | -- 多元 77 | local op = _lower(v[1]) 78 | local ra, rb = v[2], v[3] 79 | 80 | -- 闭区间 81 | if 'between' == op then 82 | return _format('`%s` between %s and %s', k, _var(ra), _var(rb)) 83 | 84 | -- 集合 85 | elseif 'in' == op or 'not in' == op then 86 | local list = {} 87 | for i = 2, #v do 88 | list[#list + 1] = _var(v[i]) 89 | end 90 | return _format('`%s` %s (%s)', k, op, _concat(list, ',')) 91 | 92 | -- 闭区间 93 | elseif '[]' == op then 94 | return _format('%s <= `%s` and `%s` <= %s', _var(ra), k, k, _var(rb)) 95 | 96 | -- 半闭半开区间 97 | elseif '[)' == op then 98 | return _format('%s <= `%s` and `%s` < %s', _var(ra), k, k, _var(rb)) 99 | 100 | -- 半开半闭区间 101 | elseif '(]' == op then 102 | return _format('%s < `%s` and `%s` <= %s', _var(ra), k, k, _var(rb)) 103 | 104 | -- 开区间 105 | elseif '()' == op then 106 | return _format('%s < `%s` and `%s` < %s', _var(ra), k, k, _var(rb)) 107 | 108 | -- 逻辑或 109 | elseif 'or' == op then 110 | local list = {} 111 | for i = 2, #v do 112 | list[#list + 1] = _format('`%s`=%s', k ,_var(v[i])) 113 | end 114 | return _concat(list, ' or ') 115 | 116 | -- < <= >= 等 117 | elseif 2 == #v then 118 | return _format('%s %s %s', k, op, _var(ra)) 119 | end 120 | 121 | return '', 'condition:' .. k .. ' nonsupport' 122 | end 123 | 124 | 125 | -- 126 | -- 127 | -- 128 | local _fields = function (fields) 129 | if _isempty(fields) then 130 | return '*' 131 | elseif 'table' == type(fields) then 132 | return '`' .. _concat(fields, '`,`') .. '`' 133 | end 134 | return fields 135 | end 136 | 137 | 138 | -- 139 | -- 140 | -- 141 | local _values = function (vals) 142 | local vls = {} 143 | for _, v in pairs(vals) do 144 | vls[#vls + 1] = _var(v) 145 | end 146 | return _concat(vls, ',') 147 | end 148 | 149 | 150 | -- 151 | -- 条件处理 152 | -- 153 | 154 | local _cond_s = function (cond) 155 | return 'string' == type (cond) 156 | and cond 157 | or nil 158 | end 159 | 160 | -- {'string :key ::key', {key = 'val'}} -- ::key : 不进行安全过虑 161 | local _cond_s_t = function (cond) 162 | if 'table' ~= type (cond) then 163 | return nil 164 | end 165 | if not ('string' == type (cond[1]) and 'table' == type (cond[2])) then 166 | return nil 167 | end 168 | local vals = cond[2] 169 | return _gsub(cond[1], '(::?)([a-zA-Z_0-9]+)', function (pre, key) 170 | if ':' == pre then 171 | return _var(vals[key]) or '' 172 | else 173 | return vals[key] or '' 174 | end 175 | end) 176 | end 177 | 178 | -- {'string ? ? ??...', v1, v2 ...} -- 占位符 ??:不进行安全过虑 179 | local _cond_s_s = function (cond) 180 | if 'table' ~= type (cond) then 181 | return nil 182 | end 183 | if 'string' ~= type (cond[1]) then 184 | return nil 185 | end 186 | local n = 1 187 | return _gsub(cond[1], '%?%??', function (pre) 188 | n = n + 1 189 | if '?' == pre then 190 | return _var(cond[n]) or '' 191 | else 192 | return cond[n] or '' 193 | end 194 | end) 195 | end 196 | 197 | 198 | -- {[1]='and | or', key = val} 199 | local _cond_k_v = function (cond) 200 | if 'table' ~= type (cond) or nil ~= cond[2] then 201 | return nil 202 | end 203 | 204 | local wh = _tabnew(10, 0) 205 | for k, v in pairs(cond) do 206 | local typ = type(v) 207 | -- 1 key 特列不处理 208 | if 1 == k then 209 | -- 210 | -- 211 | elseif 'table' == typ then 212 | wh[#wh+1] = _ext_operator(k, v) 213 | 214 | elseif ngx.null == v then 215 | wh[#wh+1] = _format('`%s` is NULL', k) 216 | 217 | else 218 | wh[#wh+1] = _format('`%s`=%s', k, _var(v)) 219 | end 220 | end 221 | 222 | if _isempty(wh) then 223 | return '' 224 | end 225 | return _concat(wh, ' '.. (cond[1] or 'and') .. ' ') 226 | end 227 | 228 | 229 | -- 错误参数 230 | local _cond_err = function () 231 | error('cond params error') 232 | end 233 | 234 | local _cond_sizer = { 235 | _cond_k_v, 236 | _cond_s, 237 | _cond_s_t, 238 | _cond_s_s, 239 | 240 | -- _cond_err, 241 | } 242 | 243 | 244 | -- 245 | -- 条件轮询器 246 | -- 247 | local _cond = function (cond) 248 | for _, sizer in ipairs(_cond_sizer) do 249 | local where = sizer(cond) 250 | if not _isempty(where) then 251 | return ' where ' .. where 252 | end 253 | end 254 | return '' 255 | end 256 | 257 | 258 | -- 259 | -- 260 | -- 261 | local _setter = function (ps) 262 | if 'table' ~= type(ps) then 263 | return '' 264 | end 265 | 266 | local wh = {} 267 | for k, v in pairs(ps) do 268 | local typ = type(v) 269 | wh[#wh+1] = _format('`%s`=%s', k, _var(v)) 270 | end 271 | return _concat(wh, ',') 272 | end 273 | 274 | 275 | -- 276 | -- 分割kv 277 | -- @return keys, values 字段名列表, 值列表 278 | -- 279 | local _split = function (...) 280 | local keys = {} 281 | local vals = {} 282 | 283 | -- merge 284 | for i=1, select('#', ... ) do 285 | for k, v in pairs(select(i, ... )) do 286 | if not _match(k, '^[%a_][%w_]+$') then 287 | return nil, '[' .. k .. '] not a valid field name' 288 | end 289 | if 'table' == type(v) then 290 | return nil, 'the [' .. k .. '] value cannot be a structure type' 291 | end 292 | keys[#keys + 1] = k 293 | vals[#vals + 1] = v 294 | end 295 | end 296 | return keys, vals 297 | end 298 | 299 | 300 | -- 301 | -- 302 | -- 303 | local _orderby = function (v) 304 | if _isempty(v) or 'string' ~= type(v) then 305 | return '' 306 | end 307 | return 'order by ' .. v 308 | end 309 | 310 | 311 | -- 312 | -- 313 | -- 314 | local _limit = function (v) 315 | local typ = type(v) 316 | if 'number' == typ then 317 | return 'limit ' .. v 318 | elseif 'table' == typ and 'number' == type(v[1]) then 319 | local lt = 0 < v[1] and v[1] or 1 320 | if 'number' == type(v[2]) then 321 | if 0 < v[2] then 322 | lt = (lt - 1) * v[2] .. ','..v[2] 323 | else 324 | lt = lt .. ',' .. v[2] 325 | end 326 | end 327 | return 'limit ' .. lt 328 | elseif 'string' == typ then 329 | return 'limit ' .. v 330 | elseif 'nil' == typ then 331 | return 'limit 1' 332 | end 333 | return '' 334 | end 335 | 336 | 337 | 338 | -- 339 | -- 340 | -- 341 | local _forupdate = function (v) 342 | return v and 'for update' or '' 343 | end 344 | 345 | 346 | 347 | 348 | -- 349 | -- 350 | -- 351 | local _M = { _VERSION = '1.0', } 352 | 353 | 354 | local _mt = { __index = _M } 355 | 356 | local _default_options = { 357 | -- path = 'unix socket' 358 | host = '127.0.0.1', 359 | port = 3306, 360 | database = 'mysql_db_name', 361 | prefix = '', 362 | user = 'root', 363 | password = '', 364 | charset = 'utf8', 365 | timeout = 5 * 1000, 366 | max_packet_size = 2 * 1024 * 1024, 367 | pool_size = 64, 368 | pool_idle_timeout = 900 * 1000, 369 | 370 | logger = tofu and tofu.log.n or print, 371 | } 372 | 373 | 374 | 375 | 376 | 377 | function _M.new(options) 378 | local conf_opts = options 379 | local self = {} 380 | 381 | -- function model 382 | return function (name, opts) 383 | opts = opts or conf_opts.default 384 | if 'string' == type(opts) then 385 | opts = conf_opts[opts] 386 | end 387 | local obj = _merge({}, _default_options) 388 | local ot = type(opts) 389 | if 'table' == ot then 390 | _merge(obj, opts) 391 | elseif 'function' == ot then 392 | obj._parser = opts 393 | else 394 | error('model-mysql-helper options invalid') 395 | end 396 | obj.name = obj.prefix .. (name or '') 397 | setmetatable(obj, _mt) 398 | return _functional(obj) 399 | end 400 | end 401 | 402 | 403 | -- 404 | -- 405 | -- 406 | function _M:model(name, opts) 407 | self.name = name 408 | return self 409 | end 410 | 411 | 412 | -- 413 | -- 414 | -- 415 | function _M:ignore(ignore) 416 | self._ignore = not not ignore 417 | return self 418 | end 419 | 420 | 421 | -- 422 | -- 423 | -- 424 | function _M:getconn(options) 425 | if options then 426 | _merge(self, options) 427 | end 428 | 429 | local conn = rawget(self, '_conn') 430 | if conn then 431 | return conn 432 | end 433 | 434 | local conn, err = _mysql:new() 435 | if not conn then 436 | error('failed to instantiate mysql:' .. err) 437 | end 438 | conn:set_timeout(self.timeout or 5000) 439 | local st = _clock() 440 | local ok, err = conn:connect(self) 441 | if not ok then 442 | -- self.logger('failed to connect database ' .. err) 443 | error('failed to connect database ' .. err) 444 | return nil, err 445 | end 446 | -- 第一次连接时才log 447 | if self.logconnect and conn:get_reused_times() < 1 then 448 | self.logger(_format( 449 | 'mysql connect: %s@%s:%s/%s -- %.2fms', 450 | self.user, 451 | self.host, 452 | self.port, 453 | self.database, 454 | (_clock() - st) / 1000 455 | )) 456 | end 457 | return conn 458 | end 459 | 460 | 461 | -- 462 | -- @param sql string | {'string ?', ...} | {'string :key', {key=value} } 463 | -- 464 | function _M:exec(sql, options) 465 | if 'string' ~= type (sql) then 466 | sql = _cond_s_t(sql) or _cond_s_s(sql) 467 | end 468 | 469 | local conn, err = rawget(self, '_conn') 470 | if not conn then 471 | if not options and self._parser then 472 | options = self._parser(sql) 473 | end 474 | 475 | conn, err = _M.getconn(self, options) 476 | if not conn then 477 | return nil, err 478 | end 479 | end 480 | 481 | local st = _clock() 482 | local res, err, errcode, sqlstate = conn:query(sql) 483 | if self.logsql and self.logger then 484 | local tail = _format('-- %.2fms', (_clock() - st) / 1000) 485 | self.logger(sql, tail) 486 | end 487 | 488 | if not res then 489 | self.logger('bad result: ' .. err, errcode, sqlstate) 490 | -- 在 trans 事务中 491 | if self._istrans then error(err) end 492 | return nil, err 493 | end 494 | 495 | if 'again' == err then 496 | local arr = {res} 497 | 498 | while 'again' == err do 499 | res, err, errcode, sqlstate = conn:read_result() 500 | if not res then 501 | self.logger('bad result: ' .. err, errcode, sqlstate) 502 | break; 503 | end 504 | arr[#arr + 1] = res 505 | end 506 | res = arr 507 | end 508 | -- 如果不在事务中则keepalive 509 | if not self._conn then 510 | local ok, err = conn:set_keepalive(self.pool_idle_timeout, self.pool_size) 511 | if not ok then 512 | self.logger('failed to keep alive: ' .. err) 513 | end 514 | end 515 | return res 516 | end 517 | 518 | 519 | 520 | -- 521 | -- @param cond 522 | -- @param opts { 523 | -- field, 524 | -- limit, : nil | number | table {number, number} | false 525 | -- orderby, 526 | -- forupdate, 527 | -- } 528 | -- 529 | function _M:get(cond, opts) 530 | opts = opts or {} 531 | local sql = 'select %s from `%s` %s %s %s %s' 532 | sql = _format(sql, _fields(opts.field), self.name, 533 | _cond(cond), 534 | _orderby(opts.orderby), 535 | _limit(opts.limit), 536 | _forupdate(opts.forupdate) 537 | ) 538 | local res, err = _M.exec(self, sql) 539 | if res then 540 | if nil == opts.limit then 541 | return res[1], nil 542 | else 543 | return setmetatable(res, _json_array_mt), nil 544 | end 545 | else 546 | return nil, err 547 | end 548 | end 549 | 550 | 551 | -- 552 | -- 553 | -- 554 | -- @restun id, err id -- nil:error, 0:update/new, n:new id 555 | -- 556 | function _M:set(cond, pair, add) 557 | assert(not _isempty(cond), 'the update condition cannot be nul') 558 | assert(not _isempty(pair), 'the update target cannot be nul') 559 | 560 | -- 添加模式: 不存在则添加 561 | if add then 562 | local fvs = _merge({}, cond, pair) 563 | if 'table' == type(add) then 564 | _merge(fvs, add) 565 | end 566 | local fs, vs = _split(fvs) 567 | if not fs then 568 | return fs, vs 569 | end 570 | local sql = 'insert%sinto `%s` (%s) values (%s) on duplicate key update %s' 571 | sql = _format(sql, self._ignore and ' ignore ' or ' ', 572 | self.name, _fields(fs), _values(vs), _setter(pair)) 573 | local res, err = _M.exec(self, sql) 574 | if not res then 575 | return nil, err 576 | end 577 | return res.affected_rows, res.insert_id 578 | 579 | -- 更新模式 580 | else 581 | local sql = 'update `%s` set %s %s' 582 | sql = _format(sql, self.name, _setter(pair), _cond(cond)) 583 | local res, err = _M.exec(self, sql) 584 | return res and res.affected_rows, err 585 | end 586 | end 587 | 588 | 589 | 590 | -- 591 | -- @restun id, err id -- nil:error, 0:update/new, n:new id 592 | -- 593 | function _M:add(...) 594 | local pair = select(1, ...) 595 | assert('table' == type(pair), 'bad argument #1 (table expected, got '.. type(pair) ..')') 596 | local sql = 'insert%sinto `%s` (%s) values (%s)' 597 | local fs, vs = _split(pair) 598 | if not fs then 599 | return fs, vs 600 | end 601 | local vs_list = {_values(vs)} 602 | -- 多行 603 | for i=2, select('#', ...) do 604 | local obj = select(i, ...) 605 | if 'table' ~= type(obj) then 606 | return nil, 'bad argument #' .. (i + 1) .. '(table expected, got '.. type(obj) .. ')' 607 | end 608 | local ivs = {} 609 | for _, k in ipairs(fs) do 610 | if nil == obj[k] then 611 | ivs[#ivs + 1] = ngx.null 612 | else 613 | ivs[#ivs + 1] = obj[k] 614 | end 615 | end 616 | vs_list[#vs_list + 1] = _values(ivs) 617 | end 618 | 619 | sql = _format(sql, self._ignore and ' ignore ' or ' ', 620 | self.name, _fields(fs), _concat(vs_list, '),(')) 621 | local res, err = _M.exec(self, sql) 622 | return res and res.insert_id, err 623 | end 624 | 625 | 626 | 627 | 628 | 629 | function _M:del(cond) 630 | if not cond then 631 | return nil, 'bad argument #1' 632 | end 633 | local sql = 'delete from `%s` %s' 634 | sql = _format(sql, self.name, _cond(cond)) 635 | local res, err = _M.exec(self,sql) 636 | return res and res.affected_rows, err 637 | end 638 | 639 | -- ---------------------------------- 640 | -- 事务相关 641 | -- 642 | function _M:begin(options) 643 | local conn = self:getconn(options) 644 | if not conn then 645 | return nil 646 | end 647 | self._conn = conn 648 | return _M.exec(self, 'BEGIN') 649 | end 650 | 651 | 652 | -- 653 | -- 654 | -- 655 | function _M:rollback() 656 | _M.exec(self, 'ROLLBACK') 657 | if self._conn then 658 | local ok, err = self._conn:set_keepalive(self.pool_idle_timeout, self.pool_size) 659 | if not ok then 660 | self.logger('failed to keep alive: ' .. err) 661 | end 662 | self._conn = nil 663 | end 664 | end 665 | 666 | 667 | -- 668 | -- 669 | -- 670 | function _M:commit() 671 | _M.exec(self, 'COMMIT') 672 | if self._conn then 673 | local ok, err = self._conn:set_keepalive(self.pool_idle_timeout, self.pool_size) 674 | if not ok then 675 | self.logger('failed to keep alive: ' .. err) 676 | end 677 | self._conn = nil 678 | end 679 | end 680 | 681 | 682 | -- 683 | -- @return ok, result 684 | -- 685 | function _M:trans(fn, ...) 686 | self._istrans = true 687 | _M.begin(self) 688 | local ok, res = pcall(fn, ...) 689 | if ok then 690 | _M.commit(self) 691 | else 692 | self.logger('error: ', res) 693 | _M.rollback(self) 694 | end 695 | self._istrans = nil 696 | return ok, res 697 | end 698 | 699 | 700 | 701 | 702 | return _M 703 | 704 | 705 | 706 | -------------------------------------------------------------------------------- /lib/resty/tofu/radio.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @file radio.lua 3 | -- @author d 4 | -- @version 0.1.1 5 | -- @brief 低频广播消息服务 6 | -- 7 | -- 依赖: 8 | -- lua_shared_dict 9 | -- 10 | -- --------------------------- 11 | -- example 12 | -- 13 | -- 创建频道 14 | -- 1) M.new('chan name') 15 | -- 2) M.turnon('chan name') 16 | -- 17 | -- 18 | -- 广播消息 19 | -- m:broadcast( 'string msg' ) 20 | -- ... 21 | -- 22 | 23 | 24 | local _semaphore = require 'ngx.semaphore' 25 | local _abs = math.abs 26 | 27 | 28 | local _M = { _VERSION = 0.1 } 29 | 30 | 31 | local _KEY_IDX = ':idx' 32 | local _KEY_SN = ':sn' 33 | local _KEY_DATA_ = ':data:' 34 | 35 | local _conf = { 36 | store = 'radio', 37 | maxn = 2^52, 38 | ttl = 0, 39 | chan = '_', 40 | } 41 | 42 | local _chans = {} 43 | 44 | 45 | -- 46 | -- 47 | -- 48 | local function _chan_new(opts) 49 | local obj = {} 50 | obj.store = opts.store or _conf.store 51 | obj.maxn = opts.maxn or _conf.maxn 52 | obj.chan = opts.chan or _conf.chan 53 | obj.key_idx = obj.chan .. _KEY_IDX 54 | obj.key_sn = obj.chan .. _KEY_SN 55 | obj.key_data_ = obj.chan .. _KEY_DATA_ 56 | obj.shd = ngx.shared[obj.store] 57 | obj.idx = obj.shd:get(obj.key_idx) or 0 58 | obj.sema = _semaphore.new() 59 | 60 | return obj 61 | end 62 | 63 | 64 | 65 | 66 | -- 67 | -- 68 | -- 69 | local function _turnon(opts) 70 | if 'table' ~= type (opts) then 71 | opts = { chan = tostring (opts or _conf.chan)} 72 | end 73 | 74 | local chan = _chans[opts.chan] 75 | if not chan then 76 | chan = _chan_new(opts) 77 | _chans[opts.chan] = chan 78 | end 79 | 80 | return chan, opts 81 | end 82 | 83 | 84 | 85 | 86 | -- 87 | -- 88 | -- 89 | function _M.turnon(opts) 90 | _turnon(opts) 91 | end 92 | 93 | 94 | 95 | 96 | -- 97 | -- @param opts string | table 98 | -- string: channel name 99 | -- table: { 100 | -- chan : string channel name 101 | -- store : string shared DICT name default:radio 102 | -- ttl : float time-to-live in seconds default: 0 103 | -- maxn : int how many messages can be cached at most 104 | -- } 105 | -- 106 | function _M.new(opts) 107 | local chan, opts = _turnon(opts) 108 | local obj = { 109 | ttl = opts.ttl or _conf.ttl, 110 | sn = chan.shd:incr(chan.key_sn, 1, 0), 111 | _chan = opts.chan or _conf.chan, 112 | _idx = chan.shd:incr(chan.key_idx, 0, 0), 113 | } 114 | 115 | return setmetatable(obj, {__index = _M}) 116 | end 117 | 118 | 119 | 120 | -- 121 | -- 获取当前 chan 名称 122 | -- 123 | function _M:chan() 124 | return self._chan 125 | end 126 | 127 | 128 | 129 | 130 | -- 131 | -- 广播一个消息 132 | -- 133 | function _M:broadcast(msg) 134 | local chan = _chans[self._chan] 135 | if not chan then 136 | return false, 'chan not exist' 137 | end 138 | 139 | local shd = chan.shd 140 | local idx = chan.shd:incr(chan.key_idx, 1, 0) 141 | if chan.maxn <= idx then 142 | -- need lock !!! 143 | idx = 0 144 | shd:set(chan.key_idx, idx) 145 | end 146 | 147 | chan.idx = idx 148 | shd:set(chan.key_data_ .. idx, msg, self.ttl) 149 | local count = chan.sema:count() 150 | if count < 0 then 151 | chan.sema:post(_abs(count)) 152 | end 153 | 154 | return true 155 | end 156 | 157 | 158 | 159 | 160 | -- 161 | -- wait and return message 162 | -- 163 | -- @return msg, chan, err 164 | -- 165 | function _M:ready() 166 | local chan = _chans[self._chan] 167 | if not chan then 168 | return nil, self._chan, 'chan not exist' 169 | end 170 | 171 | -- 再次检查 idx 的更新 (必须) 172 | -- 并发会使 self._idx > chan.idx 173 | if self._idx ~= chan.idx then 174 | chan.idx = chan.shd:get(chan.key_idx) 175 | end 176 | 177 | repeat until self._idx ~= chan.idx or chan.sema:wait(10) 178 | self._idx = (self._idx + 1) % chan.maxn 179 | return chan.shd:get(chan.key_data_ .. self._idx), chan.chan 180 | end 181 | 182 | 183 | 184 | 185 | -- 186 | -- turn channel 187 | -- 188 | function _M:turn(ch) 189 | local chan = _chans[ch] 190 | if not chan then 191 | return nil, 'chan not exist' 192 | end 193 | self._chan = ch 194 | -- self._idx = chan.shd:get(chan.key_idx) 195 | self._idx = chan.shd:incr(chan.key_idx,0, 0) 196 | return true 197 | end 198 | 199 | 200 | 201 | 202 | -- 203 | -- get history message 204 | -- 205 | function _M:history(n) 206 | local msgs = {} 207 | local chan = _chans[self:chan()] 208 | if not chan then 209 | return nil, 'chan not exist' 210 | end 211 | 212 | local idx 213 | if n <= self._idx then 214 | idx = self._idx - n 215 | else 216 | idx = chan.maxn - n + self._idx 217 | end 218 | 219 | for i=1, n do 220 | local msg = chan.shd:get(chan.key_data_ .. ((idx + i) % chan.maxn)) 221 | if msg then 222 | msgs[#msgs+1] = msg 223 | end 224 | end 225 | 226 | return msgs 227 | end 228 | 229 | 230 | 231 | -- 232 | -- synchronization frequency between workers 233 | -- 234 | 235 | local _wsrate = 1 / 24 236 | 237 | 238 | local function _worker_sync () 239 | for _, chan in pairs (_chans) do 240 | local curidx = chan.shd:get(chan.key_idx) or 0 241 | if chan.idx ~= curidx then 242 | chan.idx = curidx 243 | local count = chan.sema:count() 244 | if count < 0 then 245 | chan.sema:post(_abs(count)) 246 | end 247 | end 248 | end 249 | end 250 | 251 | 252 | ngx.timer.every(_wsrate, _worker_sync) 253 | 254 | 255 | return _M 256 | 257 | 258 | -------------------------------------------------------------------------------- /lib/resty/tofu/refile.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @file refile.lua 3 | -- @author d 4 | -- @version 0.1.0 5 | -- @brief 文件加载管理器 6 | -- 7 | 8 | 9 | local _M = { _VERSION='0.1.0' } 10 | 11 | 12 | 13 | local _cache = {} 14 | 15 | 16 | -- 17 | -- @param f string 完整文件路径 18 | -- 19 | function _M.reload(f) 20 | local ok, h = pcall(dofile, f) 21 | if not ok then 22 | return nil, h 23 | end 24 | _cache[f] = h 25 | return h 26 | end 27 | 28 | 29 | function _M.remove(f) 30 | if _cache[f] then 31 | _cache[f] = nil 32 | end 33 | return true 34 | end 35 | 36 | 37 | 38 | -- 39 | -- @param f string 完整文件路径 40 | -- @param default any 缺省 41 | -- 42 | function _M.load(f, default) 43 | local h = _cache[f] 44 | if nil == h then 45 | return _M.reload(f) 46 | end 47 | return h 48 | end 49 | 50 | 51 | 52 | 53 | return _M 54 | 55 | -------------------------------------------------------------------------------- /lib/resty/tofu/util.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @file util.lua 3 | -- @author d 4 | -- @version 0.1.1 5 | -- @brief tofu开箱即用工具箱 6 | -- 7 | 8 | local _M = { _VERSION = '0.1.1' } 9 | 10 | 11 | local _table_insert = table.insert 12 | local _table_concat = table.concat 13 | local _table_sort = table.sort 14 | 15 | local _str_reverse = string.reverse 16 | local _str_find = string.find 17 | local _str_sub = string.sub 18 | local _str_gsub = string.gsub 19 | local _str_match = string.match 20 | local _str_gmatch = string.gmatch 21 | local _str_format = string.format 22 | local _str_byte = string.byte 23 | local _str_char = string.char 24 | local _str_rep = string.rep 25 | 26 | local _ffi = require("ffi") 27 | 28 | 29 | 30 | -- ---------------------------------- 31 | -- 其它 32 | -- 33 | 34 | function _M.isempty(obj) 35 | local t = type(obj) 36 | return nil == obj 37 | or '' == obj 38 | or 'table' == t and next(obj) == nil 39 | or 'userdata' == t and ngx and ngx.null == obj 40 | end 41 | 42 | 43 | function _M.isint(v) 44 | return 'number' == type(v) and v % 1 == 0 45 | end 46 | 47 | 48 | function _M.isfloat(v) 49 | return 'number' == type(v) and v % 1 ~= 0 50 | end 51 | 52 | 53 | 54 | -- 55 | -- 是否可执行 56 | -- 57 | function _M.iscallable(f) 58 | local t = type(f) 59 | if 'function' == t then 60 | return true 61 | end 62 | 63 | if 'table' == t then 64 | local mt = getmetatable(f) 65 | return 'table'== type(mt) and 'function' == type(mt.__call) 66 | end 67 | 68 | return false 69 | end 70 | 71 | 72 | -- ---------------------------------- 73 | -- 字符串处理 74 | -- 75 | 76 | -- 77 | -- 分割字符串 78 | -- 79 | function _M.split(str, delimiter) 80 | local result = {} 81 | for match in (str .. delimiter):gmatch('(.-)'..delimiter) do 82 | _table_insert(result, match) 83 | end 84 | return result 85 | end 86 | 87 | function _M.msplit(str, sep) 88 | if not sep then 89 | sep = '%s+' 90 | end 91 | local result = {} 92 | for match in str:gmatch('([^'..sep..']+)') do 93 | _table_insert(result, match) 94 | end 95 | return result 96 | end 97 | 98 | 99 | -- 100 | -- 101 | -- 102 | function _M.dirname(str) 103 | if not str then 104 | str = debug.getinfo(2, 'S').source:sub(2, -1) 105 | end 106 | return _str_match(str, '(.*/)') or '' 107 | end 108 | 109 | -- * 比上面快10+倍 110 | -- 截取路径(/结尾) 111 | -- str = nil 取当前调用者的路径 112 | function _M.getpath(str) 113 | if not str then 114 | str = debug.getinfo(2, 'S').source:sub(2, -1) 115 | end 116 | local i= #str 117 | while 0 < i and '/' ~= _str_sub(str, i, i) do i = i - 1 end 118 | return i and _str_sub(str, 1, i) 119 | end 120 | 121 | 122 | -- 去两端空白 123 | -- 长字符串比 match 效率好点 124 | function _M.trim(str) 125 | -- return _str_gsub(_str_gsub(str, '^%s+', ''), '%s+$', '') 126 | return (_str_gsub(str, '^%s+(.-)%s+$', '%1')) 127 | end 128 | 129 | 130 | 131 | 132 | -- 133 | -- 134 | -- 135 | function _M.bin_hex(s) 136 | return (_str_gsub(s, '(.)', function (x) return _str_format('%02x', _str_byte(x)) end)) 137 | end 138 | 139 | 140 | function _M.hex_bin(s) 141 | return (_str_gsub(s, '(..)', function(x) return _str_char(tonumber(x,16)) end)) 142 | end 143 | 144 | 145 | 146 | 147 | -- 148 | -- 变量替换 149 | -- 'string ${var}' ==> string xxx 150 | -- 151 | function _M.envsubst(str, env) 152 | env = env or {} 153 | return (_str_gsub(str, '%${%s*([%w_]*)%s*}', function(key) return env[key] or '' end)) 154 | end 155 | 156 | 157 | -- ----------------------------------- 158 | -- 时间日期 159 | -- 160 | 161 | if not pcall(_ffi.typeof, 'struct timeval') then 162 | 163 | _ffi.cdef [[ 164 | struct timeval { 165 | long int tv_sec; 166 | long int tv_usec; 167 | }; 168 | 169 | int gettimeofday(struct timeval *tv, void *tz); 170 | ]] 171 | 172 | end 173 | 174 | local __c_s_tm = _ffi.new("struct timeval") 175 | local __c_f_gettimeofday = _ffi.C.gettimeofday 176 | 177 | -- 178 | -- 获取微秒级时间戳 179 | -- 180 | function _M.getusec() 181 | __c_f_gettimeofday(__c_s_tm,nil); 182 | local sec = tonumber(__c_s_tm.tv_sec); 183 | local uec = tonumber(__c_s_tm.tv_usec); 184 | return sec * 1000000 + uec; 185 | -- return sec + uec * 10^-6; 186 | -- return sec, uec; 187 | end 188 | 189 | 190 | 191 | -- 192 | -- 获取当前时区 193 | -- 194 | local _os_time = os.time() 195 | local _timezone = os.difftime(_os_time, os.time( os.date('!*t', _os_time) ) ) / 3600 196 | 197 | function _M.gettimezone() 198 | return _timezone 199 | end 200 | 201 | -- 202 | -- 日期字符串转换时间戳(秒) 203 | -- @param str 'yyyy-mm-dd hh:mm:ss' | 'yyyy/m/d' | '00:00:00' 204 | -- @param tz 时区, default: 0 205 | -- @return int 206 | function _M.tosecond(str, tz) 207 | local y, m, d = _str_match(str, '(%d+)[/%-]+(%d+)[/%-]+(%d+)') 208 | local h, i, s = _str_match(str, '(%d+):(%d+):(%d+)') 209 | if not ((y and m and d) or (h and i and s)) then 210 | return nil 211 | end 212 | return os.time({ 213 | year = y or 1970, 214 | month = m or 1, 215 | day = d or 1, 216 | hour = (h or 0) + _timezone - ( tz or 0), -- 时区处理 217 | min = i or 0, 218 | sec = s or 0, 219 | }) 220 | end 221 | 222 | 223 | 224 | 225 | -- ---------------------------------- 226 | -- table 227 | -- 228 | 229 | function _M.tab_merge(...) 230 | local ret = select(1, ...) or {} 231 | for i=2, select('#', ...) do 232 | local t = select(i, ...) 233 | if t then 234 | for k, v in pairs(t) do 235 | if 'table' == type(v) then 236 | if 'table' ~= type(ret[k]) then ret[k] = {} end 237 | ret[k] = _M.tab_merge(ret[k], v) 238 | local mt = getmetatable(v) 239 | if v then setmetatable(ret[k], mt) end 240 | else 241 | ret[k] = v 242 | end 243 | end 244 | end 245 | end 246 | return ret 247 | end 248 | 249 | 250 | 251 | 252 | -- 253 | -- 254 | -- 255 | if pcall(require, 'table.new') then 256 | _M.tab_new = require 'table.new' 257 | else 258 | function _M.tab_new(narr, nrec) 259 | return {} 260 | end 261 | end 262 | 263 | 264 | 265 | 266 | -- 267 | -- 格式化 tab -> str 268 | -- 269 | local function _tab_str(obj, opt) 270 | if 'table' ~= type(obj) then 271 | return tostring(obj) 272 | end 273 | opt = opt or {level=1} 274 | local pretty = opt.pretty 275 | local level = opt.level or 1 276 | local indent_str = pretty and _str_rep('\t', level) or '' 277 | local tpl = {} 278 | local keys = {} 279 | for k, _ in pairs(obj) do 280 | keys[#keys + 1] = k 281 | end 282 | if opt.sort then 283 | _table_sort(keys, function(a, b) return tostring(a) < tostring(b) end) 284 | end 285 | for _, k in ipairs(keys) do 286 | local v = obj[k] 287 | if 'number' ~= type(k) then 288 | k = k .. (pretty and ': ' or ':') 289 | else 290 | k = '' 291 | end 292 | 293 | local vt = type(v) 294 | if 'table' == vt then 295 | opt.level = level + 1 296 | tpl[#tpl + 1] = _str_format('%s%s%s', indent_str, k, _tab_str(v, opt)) 297 | else 298 | if 'string' == vt then 299 | tpl[#tpl + 1] = _str_format('%s%s"%s"',indent_str, k, v) 300 | elseif 'function' == vt then 301 | tpl[#tpl + 1] = _str_format('%s%s%s',indent_str, k, 'function') 302 | else 303 | tpl[#tpl + 1] = _str_format('%s%s%s',indent_str, k, tostring(v)) 304 | end 305 | end 306 | 307 | end 308 | 309 | return pretty 310 | and '{\n' .. _table_concat(tpl, ',\n') .. '\n'.. _str_rep('\t', level - 1) .. '}' 311 | or '{' .. _table_concat(tpl, ',') .. '}' 312 | 313 | end 314 | 315 | _M.tab_str = _tab_str 316 | 317 | 318 | 319 | 320 | -- 321 | -- 函数化, pointfree 化 322 | -- 把 obj:method():method() 323 | -- 为 obj.method().method() 324 | -- 325 | local _pf_mt = { 326 | __index = function (t, k) 327 | local fn = rawget(t, k) 328 | if fn then 329 | return fn 330 | end 331 | local target = rawget(t, 0) 332 | local fn = target[k] 333 | if 'function' ~= type(fn) then 334 | return fn 335 | end 336 | local fn2 = function (...) 337 | local ret = {fn(target, ...)} 338 | if target == ret[1] then 339 | return t 340 | else 341 | return unpack(ret) 342 | end 343 | end 344 | rawset(t, k, fn2) 345 | return fn2 346 | end 347 | } 348 | 349 | function _M.functional(obj) 350 | return setmetatable({[0]=obj}, _pf_mt) 351 | end 352 | 353 | 354 | 355 | 356 | -- 357 | -- 相对调用目录中查找 358 | -- 359 | function _M.extends(t, o) 360 | if 'string' == type(t) then 361 | local info = debug.getinfo(2, 'S') 362 | local path = _M.getpath(_str_sub(info.source, 2)) or '' 363 | t = dofile(path .. t .. '.lua') 364 | end 365 | return setmetatable(o or {}, {__index = t}) 366 | end 367 | 368 | 369 | 370 | -- ----------------------------------------------------- 371 | -- 文件处理 372 | -- 373 | 374 | -- 文件/目录是否存在 375 | function _M.file_exists(path) 376 | local f = io.open(path, 'rb') 377 | return f and f:close() 378 | end 379 | 380 | 381 | 382 | -- 读取文件 383 | function _M.file_read(path) 384 | local f = io.open(path, 'rb') 385 | if not f then 386 | return nil, path .. ' file does not exist' 387 | end 388 | local data = f:read('*a') 389 | f:close() 390 | return data 391 | end 392 | 393 | 394 | -- 写文件 395 | function _M.file_write(path, data) 396 | local f = io.open(path, 'w') 397 | if not f then 398 | return nil, path .. ' file does not exist' 399 | end 400 | return f:write(data) and f:close() 401 | end 402 | 403 | 404 | 405 | -- 目录是否存在 406 | function _M.dir_exists(path) 407 | return os.execute('cd ' .. path .. ' >/dev/null 2>/dev/null') -- win 使用 /nul 408 | end 409 | 410 | 411 | 412 | -- 加载 lua 格式的配置文件 413 | function _M.conf_load(conf_file, g) 414 | local res, err = loadfile(conf_file) 415 | if not res then 416 | return nil, err 417 | end 418 | 419 | local env = setmetatable({}, {__index = g or getfenv(1)}) 420 | setfenv(res, env)() 421 | -- return setmetatable(env, {}) 422 | return env 423 | end 424 | 425 | 426 | -- -------------------------------------- 427 | -- end 428 | -- 429 | return _M 430 | 431 | -------------------------------------------------------------------------------- /lib/resty/tofu/validation.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @file validation.lua 3 | -- @author d 4 | -- @version 0.1.1 5 | -- @brief 参数验证/过虑器 6 | 7 | 8 | local _util = require 'resty.tofu.util' 9 | local _ctx = require 'resty.tofu.ctx' 10 | 11 | 12 | local _str_lower = string.lower 13 | local _str_match = string.match 14 | local _util_isint = _util.isint 15 | local _util_tosecond = _util.tosecond 16 | local _util_trim = _util.trim 17 | local _util_tab_merge = _util.tab_merge 18 | 19 | 20 | -- 21 | -- 缺省配置 22 | -- 23 | local _options = { 24 | mode = 1, -- 0-检测所有参数, 1-有无效参数时立即返回(中断后面的参数检测) 25 | trim = false, -- 去两端空白 26 | amend = false, -- 自动修正参数(需指定method),如 bool 参:true|1'true',false 0 ... 27 | errmsg = { -- 缺省错误信息 string | {}, 优先级 指定>sgring>[0] 28 | -- 特殊的, 缺省信息 29 | [0] = '参数错误', 30 | 31 | -- 指定验证器错误信息, 32 | required = '参数不能为空', -- 指定 required 验证器的错误信息 33 | } 34 | } 35 | 36 | -- 37 | -- 保留方法/属性 38 | -- 39 | local _reserved = { 40 | required= true, 41 | errmsg = true, 42 | value = true, 43 | trim = true, 44 | default = true, 45 | amend = true, 46 | method = true, 47 | } 48 | 49 | 50 | -- 51 | -- 从参数列中获得一个非空(nil)值 52 | -- 53 | local function _getnonil(...) 54 | for i=1, select('#', ...) do 55 | local v = select(i, ...) 56 | if nil ~= v then 57 | return v 58 | end 59 | end 60 | end 61 | 62 | 63 | -- 64 | -- 获值 优先级: 设置 > 参数 > 缺省 65 | -- 66 | local function _getvalue(p, r) 67 | local args = _ctx.fetch().args 68 | local m = r.method 69 | m = m and _str_lower(m) 70 | local v, a = _getnonil(r.value, m and args[m][p] or args(p), r.default) 71 | if v and _getnonil(r.trim, _options.trim) then 72 | if 'string' == type (v) then 73 | v = _util.trim(v) 74 | end 75 | end 76 | return v, m 77 | end 78 | 79 | -- 80 | -- 修改(回写) 请求参数到 ctx.args 中 81 | -- 82 | local function _setvalue(p, value, m) 83 | if nil ~= value and m then 84 | local args = _ctx.fetch().args 85 | args[m][p] = value 86 | end 87 | end 88 | 89 | 90 | -- 91 | -- 获取错误信息 92 | -- p 参数名称 93 | -- k 验证器名称 94 | -- r 规则 95 | -- opts 96 | -- 97 | local function _geterrmsg(p, k, r, opts) 98 | local rt = type (r.errmsg) 99 | local ot = type (opts.errmsg) 100 | local msg = ('table' == rt and r.errmsg[k]) or ('table' == ot and opts.errmsg[k]) 101 | if not msg then 102 | msg = ('string' == rt and r.errmsg) or ('string' == ot and opts.errmsg) 103 | end 104 | if not msg then 105 | msg = ('table' == rt and r.errmsg[0]) or ('table' == ot and opts.errmsg[0]) 106 | end 107 | -- tofu.log.w(p, msg) 108 | return msg 109 | end 110 | 111 | 112 | -- 113 | -- 验证器表 114 | -- 115 | local _validators = {} 116 | 117 | 118 | -- 119 | -- 验证器 - 必选参数 120 | -- @param v any 值 121 | -- @param cond bool 122 | -- 123 | function _validators.required(v, cond) 124 | return not cond or not not v 125 | end 126 | 127 | 128 | -- 129 | -- 验证器 - 整型 130 | -- @param v any 值 131 | -- @param cond true | {[min:int], [max:int]}, {a,b,c ... } 132 | -- 133 | function _validators.int(v, cond) 134 | v = tonumber(v) 135 | if not _util_isint(v) then return false end 136 | local t = type(cond) 137 | if 'table' == t then 138 | -- {a,b,c ... } 139 | if 0 < #cond then 140 | for i in ipairs(cond) do 141 | if v == cond[i] then 142 | return true 143 | end 144 | end 145 | return false 146 | 147 | -- {min, max} 148 | else 149 | return (not cond.min or cond.min <= v) and (not cond.max or v <= cond.max), v 150 | end 151 | elseif 'number' == t then 152 | return v == cond, v 153 | end 154 | return true == cond, v 155 | end 156 | 157 | 158 | -- 159 | -- 验证器 - 数值型 160 | -- @param v any 值 161 | -- @param cond true | {[min:int], [max:int]} 162 | -- 163 | function _validators.float(v, cond) 164 | v = tonumber(v) 165 | local t = type(cond) 166 | if 'table' == t then 167 | return (not cond.min or cond.min <= v) and (not cond.max or v <= cond.max), v 168 | elseif 'number' == t then 169 | return v == cond, v 170 | end 171 | return true == cond, v 172 | end 173 | 174 | 175 | -- 176 | -- 验证器 - 纯数字串 177 | -- @param v any 值 178 | -- @param cond true 179 | -- 180 | function _validators.digit(v, cond) 181 | return true == cond and not _str_match(v, '[^0-9]') 182 | end 183 | 184 | 185 | -- 186 | -- 验证器 - 字符串 187 | -- @param v any 值 188 | -- @param cond true | {[min:int], [max:int]} | {'a', 'b', ...} 189 | -- 190 | function _validators.string(v, cond) 191 | v = tostring(v) 192 | local t = type (cond) 193 | if 'table' == t then 194 | -- {a,b,c ...} 195 | if 0 < #cond then 196 | for i in ipairs(cond) do 197 | if v == cond[i] then 198 | return true 199 | end 200 | end 201 | return false 202 | 203 | -- {min, max} 204 | else 205 | local len = #v 206 | return (not cond.min or cond.min <= len) and (not cond.max or len <= cond.max), v 207 | end 208 | elseif 'string' == t then 209 | return v == cond, v 210 | end 211 | return true == cond, v 212 | end 213 | 214 | 215 | -- 216 | -- 验证器 - 日期 217 | -- @param v any 值 218 | -- @param cond true | {[min:date], [max:date]} 219 | -- 220 | function _validators.date(v, cond) 221 | local sec = _util_tosecond(v) 222 | if not sec then 223 | return false 224 | end 225 | if 'table' == type (cond) then 226 | return (not cond.min or (_util_tosecond(cond.min) or 253402271999) <= sec) 227 | and 228 | (not cond.max or sec <= (_util_tosecond(cond.max) or -1)) 229 | end 230 | return true == cond and 0 < sec 231 | end 232 | 233 | 234 | -- 235 | -- 验证器 - 布尔 236 | -- @param v any 值 237 | -- @param cond true 238 | -- 239 | function _validators.bool(v, cond) 240 | v = '' ~= v 241 | and '0' ~= v and 0 ~= v 242 | and 'false' ~= ('string' == type(v) and _str_lower(v)) 243 | and not not v 244 | return true == cond, v 245 | end 246 | 247 | 248 | -- 249 | -- 验证器 - 字母 250 | -- @param v any 值 251 | -- @param cond true 252 | -- 253 | function _validators.alpha(v, cond) 254 | return true == cond and not _str_match(v, '[^a-zA-Z]') 255 | end 256 | 257 | -- 258 | -- 验证器 - 十六进制hex 259 | -- @param v any 值 260 | -- @param cond true 261 | -- 262 | function _validators.hex(v, cond) 263 | return true == cond and not _str_match(v, '[^0-9a-fA-F]') 264 | end 265 | 266 | 267 | -- 268 | -- 验证器 - 字母数字 269 | -- @param v any 值 270 | -- @param cond true 271 | -- 272 | function _validators.alphanumeric(v, cond) 273 | return true == cond and not _str_match(v, '[^0-9a-zA-Z]') 274 | end 275 | 276 | 277 | 278 | -- ----------------------------------------------------------------------- 279 | 280 | 281 | -- 282 | -- @param rules : { 283 | -- : { : } 284 | -- ... 285 | -- } 286 | -- 287 | -- opts : {mode: 0-检测所有参数 | 1-当有无效参数时立即退出检测} 288 | -- 289 | local function _handle(rules, opts) 290 | opts = _util_tab_merge({}, _options, opts) 291 | if 'table' ~= type (rules) then 292 | error ('rules must be table') 293 | end 294 | local errs = {} 295 | for p, r in pairs(rules) do 296 | local v, m = _getvalue(p, r) 297 | -- 如果为nil, 验证是否为必填参数 298 | if nil == v then 299 | if r.required then 300 | errs[p] = _geterrmsg(p, 'required', r, opts) 301 | if 1 == opts.mode then return false, errs end 302 | end 303 | 304 | -- 其它验证 305 | else 306 | for k, cond in pairs(r) do 307 | if not _reserved[k] then 308 | local h = _validators[k] 309 | if not h then 310 | error ('validator '.. k .. ' not exist') 311 | end 312 | local ok, valid, v2 = pcall(h, v, cond) 313 | if not ok then 314 | -- error (valid) 315 | tofu.log.e(valid) 316 | end 317 | if true ~= valid then 318 | errs[p] = _geterrmsg(p, k, r, opts) 319 | if 1 == opts.mode then return false, errs end 320 | end 321 | if nil ~= v2 then v = v2 end 322 | end 323 | end 324 | end 325 | 326 | -- 修正参数 327 | if _getnonil(r.amend, opts.amend) then 328 | _setvalue(p, v, m) 329 | end 330 | end 331 | 332 | if not next(errs) then 333 | return true, nil 334 | else 335 | return false, errs 336 | end 337 | end 338 | 339 | 340 | local _M = {} 341 | 342 | 343 | -- -- tofu extend 接口 344 | -- function _M.new(opts) 345 | -- _util.tab_merge(_options, optins) 346 | -- return _M 347 | -- end 348 | 349 | 350 | function _M.options(opts) 351 | _util.tab_merge(_options, optins) 352 | return _M 353 | end 354 | 355 | 356 | function _M.register(fs, f) 357 | local t = type (fs) 358 | if 'table' == t then 359 | for k, f in pairs(fs) do 360 | if not _reserved[k] then 361 | _validators[k] = f 362 | end 363 | end 364 | elseif 'string' == t and 'function' == type (f) then 365 | if not _reserved[fs] then 366 | _validators[fs] = f 367 | end 368 | else 369 | error ('params error') 370 | end 371 | end 372 | 373 | _M.handle = _handle 374 | return _M 375 | 376 | 377 | -------------------------------------------------------------------------------- /lib/resty/tofu/wscontroller.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @author d 3 | -- @brief websocket controller 4 | -- 5 | -- 6 | 7 | local _tabnew = table.new 8 | local _concat = table.concat 9 | 10 | local _wss = require 'resty.websocket.server' 11 | local _merge = require 'resty.tofu.util'.tab_merge 12 | 13 | 14 | local _opts = { 15 | timeout = 5000, 16 | max_payload_len = 65535, 17 | -- send_masked = false, 18 | } 19 | 20 | 21 | 22 | local _M = {} 23 | 24 | 25 | 26 | -- 27 | -- @param m table event handler 28 | -- { 29 | -- _open(wb) --> return state 30 | -- _close(wb, state) 31 | -- _data(wb, data, state) 32 | -- _ping(wb, data, state) 33 | -- _pong(wb, data, state) 34 | -- _timeout(wb, state) 35 | -- } 36 | -- 37 | -- @param opts = { 38 | -- tiemout 39 | -- max_payload_len 40 | -- send_masked 41 | -- } 42 | -- 43 | function _M.upgrade(m, opts) 44 | assert('table' == type(m), 'argument #1 need table type') 45 | assert(not opts or 'table' == type(opts), 'argument #2 need table type') 46 | opts = _merge(opts or {}, _opts) 47 | local wb, err = _wss:new(opts) 48 | if not wb then 49 | return false, err 50 | end 51 | 52 | -- event handle 53 | local handle_open = m['_open'] -- fn(wb) 54 | local handle_close = m['_close'] -- fn(wb, state) 55 | local handle_data = m['_data'] -- fn(wb, data, state) 56 | local handle_ping = m['_ping'] -- fn(wb, data, state) 57 | local handle_pong = m['_pong'] -- fn(wb, data, state) 58 | local handle_timeout = m['_timeout'] -- fn(wb, state) 59 | 60 | local state = nil 61 | local buf = _tabnew(8, 0) 62 | 63 | -- event: on open 64 | if handle_open then 65 | state = handle_open(wb) 66 | end 67 | 68 | repeat 69 | local data, typ, err = wb:recv_frame() 70 | -- ws event: on data 71 | if data and ('text' == typ or 'binary' == typ) then 72 | if handle_data then 73 | handle_data(wb, data, state) 74 | end 75 | 76 | -- data fragmentation (more frames data) 77 | elseif 'continuation' == typ then 78 | buf[#buf + 1] = data 79 | if not err then 80 | data = _concat(buf) 81 | buf = _tabnew(8, 0) 82 | if handle_data then 83 | handle_data(wb, data, state) 84 | end 85 | end 86 | 87 | -- ws event: on ping 88 | elseif 'ping' == typ then 89 | if handle_ping then 90 | handle_ping(wb, data, state) 91 | else 92 | wb:send_pong(data) 93 | end 94 | 95 | -- ws event: on pong 96 | elseif 'pong' == typ then 97 | if handle_pong then 98 | handle_pong(wb, data, state) 99 | end 100 | -- just discard the incoming pong frame 101 | 102 | -- ws event: on timeout 103 | else 104 | if handle_timeout then 105 | handle_timeout(wb, state) 106 | end 107 | end 108 | until 'close' == typ 109 | 110 | -- event: on close 111 | if handle_close then 112 | handle_close(wb, state) 113 | end 114 | 115 | end 116 | 117 | 118 | 119 | 120 | return _M 121 | 122 | -------------------------------------------------------------------------------- /lib/resty/tofu/x6.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- @file x6.lua 3 | -- @author d 4 | -- @version 0.1.1 5 | -- @brief 中间件发动机 6 | -- 7 | 8 | local _M = { _VERSION = '0.1.1' } 9 | local _mt = { __index = _M } 10 | 11 | 12 | -- 13 | -- 14 | -- 15 | local function _iscallable(t) 16 | local t = type (t) 17 | if 'table' == t then 18 | local mt = getmetatable(t) or {} 19 | t = type (mt.__call) 20 | end 21 | return 'function' == t 22 | end 23 | 24 | 25 | -- 26 | -- new 27 | -- 28 | -- @return x6 29 | -- 30 | function _M.new() 31 | local obj = { 32 | _middleware = {} 33 | } 34 | return setmetatable(obj, _mt) 35 | end 36 | 37 | 38 | 39 | 40 | -- 41 | -- 添加中间件 42 | -- @param fn 43 | -- 44 | function _M:use(fn) 45 | if not _iscallable(fn) then 46 | error('middleware must de a function or callable') 47 | end 48 | table.insert(self._middleware, fn) 49 | return self 50 | end 51 | 52 | 53 | 54 | 55 | -- 56 | -- 开始处理 57 | -- 58 | function _M:handle(ctx) 59 | ctx = ctx or {} 60 | local n = 0 61 | local middleware = self._middleware 62 | local function _flow() 63 | n = n + 1 64 | local fn = middleware[n] 65 | if fn then 66 | fn(ctx, _flow) 67 | end 68 | end 69 | _flow() 70 | end 71 | 72 | 73 | 74 | 75 | 76 | return _M 77 | 78 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 |

tofu

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | # lua-resty-tofu 15 | 16 | 17 | 18 | tofu(豆腐) 应用 framework, 开箱即用,使得基于openresty开发应用时更加丝般顺滑 19 | 20 | 21 | 22 | [TOC] 23 | 24 | 25 | 26 | ## 状态 27 | 28 | alpha 阶段, 目前仅支持 linux/unix 环境 29 | 30 | 31 | 32 | 33 | 34 | ## 特性 35 | 36 | * 基于openresty 37 | * middleware方式流程处理 38 | * 所有功能使用extend方式 39 | * 内置常用工具包脚手架等,方便开发 40 | 41 | 42 | 43 | 44 | 45 | ## 开速开始 46 | 47 | openresty 在提供脚手架方面非常有限,请确保环境已安装好 48 | 49 | * [openresty](http://openresty.org/en/installation.html) 50 | * [opm](http://openresty.org/en/installation.html) 51 | * [luarocks (可选)](https://luarocks.org/) 52 | 53 | 54 | 55 | ### 创建项目 56 | 57 | > 使用项目工程模板 详细查看 [tofu-project-default](https://github.com/d80x86/tofu-project-default) 58 | 59 | ```sh 60 | ## 从github中clone 61 | git clone --depth=1 https://github.com/d80x86/tofu-project-default.git new_name 62 | 63 | ## 进入项目 64 | cd new_name 65 | 66 | ## 安装tofu framework 67 | ./tofu install 68 | 69 | ``` 70 | 71 | 72 | 73 | ### 可选操作 74 | 75 | ```sh 76 | ## 移除无用的旧git信息 77 | rm -rf .git 78 | 79 | ## 添加 80 | echo '/lua_modules' >> .gitignore 81 | 82 | ## 重新添加git信息 83 | git init 84 | ``` 85 | 86 | 87 | 88 | ### 启动项目(开发) 89 | 90 | ```sh 91 | ./tofu console 92 | ``` 93 | 94 | 如果可以看到类似这样的提示, 恭喜🎉 95 | 96 | ```sh 97 | 2021/08/18 19:56:49 [notice] 2482#2482: using the "epoll" event method 98 | 2021/08/18 19:56:49 [notice] 2482#2482: openresty/1.19.3.2 99 | 2021/08/18 19:56:49 [notice] 2482#2482: built by gcc 9.3.1 20200408 (Red Hat 9.3.1-2) (GCC) 100 | 2021/08/18 19:56:49 [notice] 2482#2482: OS: Linux 3.10.0-957.21.3.el7.x86_64 101 | 2021/08/18 19:56:49 [notice] 2482#2482: getrlimit(RLIMIT_NOFILE): 100001:1000000 102 | 2021/08/18 19:56:49 [notice] 2482#2482: start worker processes 103 | 2021/08/18 19:56:49 [notice] 2482#2482: start worker process 2483 104 | 2021/08/18 19:56:49 [notice] 2482#2482: start worker process 2484 105 | 2021/08/18 19:56:49 [notice] 2482#2482: start privileged agent process 2485 106 | 2021-08-18 19:56:49 [notice] tofu version: 0.1.1 107 | 2021-08-18 19:56:49 [notice] environment: development 108 | 2021-08-18 19:56:49 [notice] listen: 9527 109 | ``` 110 | 111 | 112 | 113 | ### 启动项目(生产) 114 | 115 | ```sh 116 | ./tofu start 117 | ``` 118 | 119 | > 默认使用 9527 端口 120 | > 121 | > 可在 conf/config.lua 配置中更改 122 | 123 | 124 | 125 | ### 停止服务 126 | 127 | ``` sh 128 | ./tofu stop 129 | ``` 130 | 131 | > 开发模式,直接使用control + c 终止 132 | 133 | 134 | 135 | ### 更多命令 136 | 137 | ```shell 138 | ./tofu help 139 | ``` 140 | 141 | 执行后会看到下面的信息 142 | 143 | ```sh 144 | usage:tofu COMMADN [OPTIONS]... 145 | 146 | the available commands are: 147 | console start service (development) 148 | help help 149 | install install deps to current resty_module 150 | new create a new project in the directory 151 | reload reload nginx.conf 152 | start start service (production) 153 | stop stop service 154 | version show version information 155 | 156 | ``` 157 | 158 | > 如想查看某命令的使用方式 159 | > 160 | > ./tofu help console 161 | 162 | 163 | 164 | ### 依赖管理 165 | 166 | 许多时候我们都有比较好的第三方库或解决方案选择,我们直接下载使用即可,无需做过多的重复工作。tofu一些内置组件 view , session 等有第三方依赖,如我们需要使用,只需添加相关组件配置和安装依赖即可使用。安装依赖tofu也提供简单的管理方案,只需要: 167 | 168 | 1. #### 在 tofu.package.lua 文件中添加所需的依赖 169 | 170 | ```lua 171 | -- 在配置文件 tofu.package.lua 172 | -- 添加所需要的opm包或luarocks包 173 | -- 也需要相关的 opm 或 luarocks 的使用环境, 安装方式请查阅资料 174 | 175 | -- 依赖列表 176 | -- 使用 -- string 或 {'方式:opm|luarocks', ''} 177 | deps = { 178 | 'bungle/lua-resty-template', -- 默认使用 opm 方式 179 | 'bungle/lua-resty-session', 180 | {'luarocks', 'lua-resty-jwt'} -- 指定 luarocks 方式 181 | } 182 | ``` 183 | 184 | 2. #### 然后执行 185 | 186 | ```sh 187 | ./tofu install 188 | ``` 189 | 190 | tofu 会根据 ```tofu.package.lua``` 文件中所配置 ```deps={...}``` 列表使用opm或luarocks进行安装所需要的依赖. 191 | 192 | 相关依赖安装在当前目录```lua_modules```中.(默认情况下tofu已将该目录添加到 lua_package_path 中了) 193 | 194 | 类信于下面的安装依赖包的过程信息 195 | 196 | ```text 197 | install bungle/lua-resty-template 198 | * Fetching bungle/lua-resty-template 199 | ... 200 | install bungle/lua-resty-session 201 | * Fetching bungle/lua-resty-session 202 | * Fetching openresty/lua-resty-string 203 | ... 204 | install lua-resty-jwt 205 | ... 206 | ``` 207 | 208 | 209 | 210 | 211 | 212 | ## 启动执行流程 213 | 214 | 1. 执行 ./tofu start 或 ./tofu console 215 | 2. 读取 conf/tofu.nginx.conf 配置文件 216 | 3. 根据配置初始化相关(生成最终的 nginx.conf 文件, 创建logs目录, 临时目录,缓存等目录) 217 | 4. 使用 openrsety 启动服务 218 | 5. 根据环境加载扩展配置文件 conf/extend.lua conf/extend.<环境>.lua 219 | 6. 实例化和挂载 extend 220 | 7. 根据环境加载中间件配置文件 conf/middleware.lua conf/middleware.<环境>.lua 221 | 8. 实例化和挂载 middleware 222 | 9. 服务启动完成 223 | 224 | 225 | 226 | 服务启动完成后会打印下面信息 227 | 228 | ``` text 229 | 2021-08-25 13:43:46 [notice] tofu version: 0.1.3.alpha 230 | 2021-08-25 13:43:46 [notice] environment: development 231 | 2021-08-25 13:43:46 [notice] listen: 9527 232 | 233 | ``` 234 | 235 | 236 | 237 | 238 | 239 | ## 用户请求处理流程 240 | 241 | 1. openresty 接收到用户请求调用tofu处理 242 | 2. tofu 为该请求创建一个独立的 context, 然后调用中间件处理 243 | 3. 中间件 resty.tofu.middleware.trace 只是一个简单的记录访问时间和响应时间 244 | 4. 中间件 resty.tofu.middleware.router 路由和uri参数解析处理,处理结果存在 context 中 245 | 5. 中间件 resty.tofu.middleware.payload 参数解析处理, 结果存在 context 中 246 | 6. 中间件 resty.tofu.middleware.guard 从context 中获得参数然后调用相应方法处理, 结果中断流程还是继续 247 | 7. 中间件 resty.tofu.middleware.controller 从context 中获得相应的controller 和 action 处理 248 | 249 | > 注: 所有中间件(middleware) 都是可选的, 这里只列举出较常规的处理流程 250 | 251 | 252 | 253 | 254 | 255 | ## 基础功能 256 | 257 | 258 | 259 | ### 配置 / config 260 | 261 | 框架提供了各种可扩展的配置功能,可以自动合并,按顺序覆盖,且可以根据环境维护不同的配置。 262 | 263 | 配置文件放在 conf/ 目录下,并根据作用/功能划分到不同的配置文件 264 | 265 | | 文件名 | 作用 | 可选 | 266 | | --------------- | --------------------------------------------- | :--: | 267 | | config.lua | 通用的一些配置,以及tofu.nginx.conf的变量配置 | | 268 | | extend.lua | 扩展组件配置,挂载那些扩展到tofu中使用 | | 269 | | middleware.lua | 中间件配置, 挂载那些中间件到处理流程中 | | 270 | | tofu.nginx.conf | nginx 配置 | | 271 | | task.lua | 定时任务配置 | 可选 | 272 | 273 | 274 | 275 | #### 配置格式 276 | 277 | 后缀是lua的配置文件是一个可执行的lua文件, 返回文件中的全局变量 278 | 279 | ``` lua 280 | -- 配置文件 config.lua 样例 281 | 282 | ngx_port = 9527 -- 这是一个在 tofu.nginx.conf 中使用的变量, 配置nginx的server端口 283 | 284 | ``` 285 | 286 | 287 | 288 | 配置文件约定使用一个 与 文件名相同的全局变量名 289 | 290 | ``` lua 291 | -- 配置文件 extend.lua 样例 292 | 293 | -- 约定一个lua 全局变量名称 extend 与配置文件名称相同, 仅仅是一种约定! 294 | extend = { 295 | -- 296 | -- ... 297 | -- 298 | } 299 | ``` 300 | 301 | 302 | 303 | #### 多环境配置加载与合并规则 304 | 305 | 加载目标配置文件, 加载目标环境配置文件。后加载的会覆盖或合并前面的同名配置。 306 | 307 | 文件命名格式: `[name].[env].lua` , 如 `config.development.lua`, `config.production.lua` 308 | 309 | ```lua 310 | -- 配置文件 config.lua 311 | api = { 312 | appid = '123', 313 | secret = 'asdfghjkl', 314 | } 315 | 316 | 317 | -- 配置文件 config.production.lua 318 | api = { 319 | secret = 'lkjhgfdsa', 320 | timeout = 5000, 321 | } 322 | ``` 323 | 324 | 配置加载目标配置文件 `config.lua`, 然后加载目标环境配置文件 `config.production.lua`, 后并后结果 325 | 326 | ``` lua 327 | api = { 328 | appid = '123', 329 | secret = 'lkjhgfdsa', 330 | timeout = 5000, 331 | } 332 | ``` 333 | 334 | > 更多使用方法查看扩展: `resty.tofu.extend.config` 335 | 336 | 337 | 338 | #### 特殊配置文件 339 | 340 | ```tofu.nginx.conf``` 这是个特殊的配文件,是个标准的[nginx](http://nginx.org/)配置文件。 额外支持使用 ${var_name} 变量. 341 | 342 | 如果需要更复杂的处理,可以在 `config.lua` 中 设置 `ngx_conf_file_template = true` 开启` lua-resty-template` 语法持持 (需要 库 lua-resty-template ) 支持 343 | 344 | 345 | 346 | ### 上下文 / context 347 | 348 | `context` 是处理用户请求过程中的一个对象,贯穿整个请求生命周期。简称`ctx`。 349 | 350 | 与 `ngx.ctx` 功能类似`ngx.ctx`只是要给用户使用的, 而`ctx` 主要使用于框架内使用。 351 | 352 | 如主要用于在中间件中流转 353 | 354 | 355 | 356 | ### 中间件 / middleware 357 | 358 | tofu 的中间件执行过程是基于洋葱圈模型 359 | 360 | #### 中间件格式 361 | 362 | ``` lua 363 | return function (options) 364 | 365 | return function (ctx, flow) 366 | -- 干 ... 367 | flow() -- 调用后面的中间件 368 | -- ... 369 | end 370 | end 371 | ``` 372 | 373 | 中间件格式为一个高阶阶函数,外部的函数接收一个 `options` 参数,这样方便中间件提供一些配置信息,用来开启/关闭一些功能。执行后返回另一个函数,这个函数接收 `ctx`, `flow` 参数,其中 `ctx` 为 `context` 的简写,是当前请求生命周期的一个对象,`flow` 是后续的中间件,这样可以很方便的处理后置逻辑。 374 | 375 | 中间件洋葱圈模型图: 376 | 377 | ![中间件葱图](https://camo.githubusercontent.com/d80cf3b511ef4898bcde9a464de491fa15a50d06/68747470733a2f2f7261772e6769746875622e636f6d2f66656e676d6b322f6b6f612d67756964652f6d61737465722f6f6e696f6e2e706e67) 378 | 379 | 380 | 381 | 382 | 383 | #### 中间件配置和格式 384 | 385 | 中间件的配置文件在 `<应用根目录>/conf/middleware.lua` 386 | 387 | ```lua 388 | -- 配置文件 conf/middleware.lua 389 | 390 | middleware = { 391 | 392 | -- 中间件配置样例 393 | { 394 | enable = true, -- default 395 | match = [[/api]], 396 | handle = 'resty.touf.middleware.trace', 397 | options = { 398 | logger = tofu.log.n 399 | } 400 | }, 401 | 402 | -- 快速配置方式 403 | 'resty.tofu.middleware.router', 404 | 405 | -- ... 406 | } 407 | ``` 408 | 409 | 410 | 411 | #### 通用参数: 412 | 413 | | 参数 | 类型 | 说明 | 必填 | 缺省值 | 414 | | ------- | ------------------ | --------------------------------------------------- | :--: | ------ | 415 | | handle | string \| function | 中间件, 可以是包名 或是中间件的高阶函数 | 是 | | 416 | | enable | bool | 控制中间件是否开启,主要用于控制中间件作用于某种环境 | 否 | true | 417 | | match | string \| function | 设置只有符合某些规则的请求才会进入这个中间件处理 | 否 | | 418 | | options | any | 中间件初始化时,传递给中间件函数 | 否 | | 419 | 420 | 421 | 422 | #### handle 423 | 424 | **string**: 425 | 426 | 包名字符串,如 `resty.tofu.middleware.router` ,先查找应用middleware目录下,如果没有再按`require`方式加载。 427 | 428 | ``` lua 429 | -- 配置文件 conf/middleware.lua 430 | 431 | middleware = { 432 | 433 | -- 方式一 434 | { 435 | handle = 'resty.tofu.middleware.router', 436 | }, 437 | 438 | -- 方式二 439 | 'resty.tofu.middleware.router', 440 | 441 | -- 这两种配方式效果是相同的 442 | } 443 | ``` 444 | 445 | 446 | 447 | **function**: 448 | 449 | 中间件函数, 有些比较简单或临时的中间件,我们可以直接写在配置文件中 450 | 451 | ``` lua 452 | -- 配置文件 conf/middleware.lua 453 | 454 | middleware = { 455 | 456 | -- 中间件配置样例 457 | { 458 | handle = function (options) 459 | return function (ctx, flow) 460 | tofu.log.d('业务处理') 461 | flow() 462 | end 463 | end, 464 | options = { } 465 | }, 466 | 467 | } 468 | ``` 469 | 470 | 471 | 472 | #### enable 473 | 474 | 如果我们需要控制中间件在特定的环境中开启或关闭,可以设置`enable`参数为 true | false (默认 true) 475 | 476 | ``` lua 477 | -- 配置文件 conf/middleware.lua 478 | 479 | local _isdev = 'development' == tofu.env 480 | 481 | middleware = { 482 | 483 | -- 只在开发模式中开启该中间件 484 | { 485 | enable = _isdev, 486 | handle = 'resty.tofu.middleware.trace', 487 | }, 488 | 489 | } 490 | ``` 491 | 492 | 493 | 494 | #### match 495 | 496 | **string**: 497 | 498 | uri路径正则配置,使用的是`ngx.re.match`语法 499 | 500 | ``` lua 501 | -- 配置文件 conf/middleware.lua 502 | 503 | middleware = { 504 | 505 | -- uri 以 /static 前缀开头的请求才进入处理 506 | { 507 | match = [[/static]], 508 | handle = 'xxx_middleware', 509 | }, 510 | 511 | } 512 | ``` 513 | 514 | 515 | 516 | **function**: 517 | 518 | 函数返回 true | false 结果来判断匹配 519 | 520 | ``` lua 521 | -- 配置文件 conf/middleware.lua 522 | 523 | middleware = { 524 | 525 | { 526 | handle = 'xxx_middleware', 527 | 528 | -- 只有 ios 设备才进入处理 529 | match = function (ctx) 530 | local user_agent = ngx.req.get_headers()['user-agent'] 531 | return ngx.re.match(user_agent, [[(iphone|ipad|ipod)]], 'joi') 532 | end 533 | }, 534 | 535 | } 536 | ``` 537 | 538 | 539 | 540 | #### options 541 | 542 | 中间件初始化时,透传给中间件创建函数 543 | 544 | ``` lua 545 | -- 配置文件 conf/middleware.lua 546 | 547 | middleware = { 548 | 549 | { 550 | handle = function (options) 551 | return function (ctx, flow) 552 | end 553 | end, 554 | options = { }, -- 这个参数会原样传入给上面 handle 指定的中间件 555 | }, 556 | } 557 | ``` 558 | 559 | 560 | 561 | #### 内置中间件 562 | 563 | > 框架默认内置了常用的中间件 564 | 565 | | 中间件名称 | 中间件包名 | 作用 | 566 | | ---------- | -------------------------------- | ---------------- | 567 | | trace | resty.tofu.middleware.trace | 跟踪记录请求 | 568 | | router | resty.tofu.middleware.router | 路由解析 | 569 | | payload | resty.tofu.middleware.payload | 请求参数解析 | 570 | | guard | resty.tofu.middleware.guard | 参数过滤 | 571 | | controller | resty.tofu.middleware.controller | 控制器,业务处理 | 572 | 573 | 574 | 575 | #### 其它中间件 576 | 577 | * [CORS](https://github.com/d80x86/tofu-middleware-cors) 跨域处理 578 | 579 | * [JWT](https://github.com/d80x86/tofu-middleware-jwt) 验证处理 580 | 581 | 582 | 583 | #### 自定义中间件 584 | 585 | 根据业务需要添加中间件,按约定放在 `lua/middleware` 目录下,然后就可以中间件配置中直接使用 586 | 587 | 创建自定义中间件, 添加文件 `lua/middleware/example.lua` 588 | 589 | ``` lua 590 | -- 文件lua/middleware/example.lua 591 | 592 | return function (options) 593 | return function (ctx, flow) 594 | tofu.log.d('你静鸡鸡地来了') 595 | flow() 596 | tofu.log.d('你静鸡鸡地走了') 597 | end 598 | end 599 | ``` 600 | 601 | 然后在中间件配置文件 `conf/middleware.lua` 中添加配置 602 | 603 | ``` lua 604 | -- 配置文件 conf/middleware.lua 605 | 606 | middleware = { 607 | 608 | -- 方式一 609 | { 610 | handle = 'example', 611 | options = { } 612 | }, 613 | 614 | -- 方式二 615 | 'middleware.example', 616 | } 617 | ``` 618 | 619 | 直接写在配置文件中 620 | 621 | ``` lua 622 | -- 配置文件 conf/middleware.lua 623 | 624 | middleware = { 625 | 626 | -- 自定义中间件 627 | { 628 | handle = function (options) 629 | return function (ctx, flow) 630 | tofu.log.d('业务处理') 631 | flow() 632 | end 633 | end, 634 | options = { } 635 | }, 636 | 637 | -- ... 638 | } 639 | ``` 640 | 641 | 642 | 643 | ### 扩展 / extend 644 | 645 | tofu框架可以说是一个 **零** 功能的框架,所有功能都是通过扩展得来实现。所以扩展都可以通过配置来控制。 646 | 647 | 因为openresty已提供了相关基础的很友好和方便调用的api,所以tofu没有必要再封装一套基础的api。 648 | 649 | 这也极大减少不必要的api和学习的成本。 650 | 651 | 652 | 653 | #### 扩展配置和格式 654 | 655 | 扩展的配置文件在 `<应用根目录>/conf/extend.lua` 656 | 657 | ```lua 658 | -- 配置文件 conf/extend.lua 659 | 660 | extend = { 661 | 662 | -- 扩展配置样例 663 | { 664 | enable = true, -- default 665 | named = 'config', 666 | type = 'default', 667 | default = { 668 | handle = 'resty.touf.extend.config', 669 | options = { 670 | env = tofu.env, 671 | prefix = tofu.ROOT_PATH .. 'conf/', 672 | } 673 | } 674 | }, 675 | 676 | -- 快速配置方式一 677 | 'resty.tofu.extend.builtin', 678 | 679 | -- 快速配置方式二, 等同上面方式 680 | { 681 | default = { 682 | handle = 'resty.tofu.extend.builtin', -- 还可以是 function | table 683 | } 684 | }, 685 | 686 | -- ... 687 | } 688 | ``` 689 | 690 | 691 | 692 | #### 通用参数: 693 | 694 | | 参数 | 类型 | 说明 | 必填 | 缺省值 | 695 | | ------ | ------ | ----------------------------------------------------- | :--: | :-----: | 696 | | enable | bool | 控制中间件是否开启,主要用于控制扩展组件作用于某种环境 | 否 | true | 697 | | named | string | 为设置扩展命名后可用 tofu. 全局调用 | 否 | | 698 | | type | string | 中间件初始化时,指定使用那一组参数初始化`扩展组件` | 否 | default | 699 | 700 | 701 | 702 | #### enable 703 | 704 | 如果我们需要控制扩展组件在特定的环境中开启或关闭,可以设置`enable`参数为 true | false (默认 true) 705 | 706 | ``` lua 707 | -- 配置文件 conf/extend.lua 708 | 709 | -- 是否开发环境 710 | local _isdev = 'development' == tofu.env 711 | 712 | extend = { 713 | 714 | -- 只在开发模式中开启该扩展组件 715 | { 716 | named = 'watcher', 717 | enable = _isdev, 718 | default = { 719 | trace = true, 720 | handle = 'resty.tofu.extend.jili' 721 | }, 722 | }, 723 | } 724 | ``` 725 | 726 | 727 | 728 | #### named 729 | 730 | 挂载到`tofu`的名称,使得后面可以使用 `tofu.` 方式调用。命名不能为空字符串,命名不能重复。 731 | 732 | 当为 `nil` 时,扩展组件又是` table`,则将扩展中的所有非下划`_`开头的属性加载到tofu 733 | 734 | 735 | 736 | #### type 737 | 738 | 指定扩展组件使用那一组参数 739 | 740 | ``` lua 741 | -- 配置文件 conf/extend.lua 742 | 743 | 744 | -- 是否开发环境 745 | local _isdev = 'development' == tofu.env 746 | 747 | extend = { 748 | 749 | -- 只在开发模式中开启该中间件 750 | { 751 | named = 'log', 752 | -- 根据环境指定不同类型的中间件,分别对应下面的配置 753 | type = _isdev and 'console' or 'file', 754 | 755 | -- 把日志打印到终端 756 | console = { 757 | -- ... 758 | }, 759 | 760 | -- 把日志打印到文件 761 | file = { 762 | -- ... 763 | }, 764 | }, 765 | } 766 | ``` 767 | 768 | 769 | 770 | **扩展组件参数格式** 771 | 772 | ```lua 773 | -- 配置文件 conf/extend.lua 774 | 775 | extend = { 776 | 777 | { 778 | named = 'view', 779 | 780 | -- 扩展组件参数格式 781 | default = { 782 | handle = 'resty.tofu.extend.view', 783 | options = { 784 | -- ... 785 | } 786 | } 787 | 788 | }, 789 | } 790 | ``` 791 | 792 | 793 | 794 | **type指定的扩展组件** 795 | 796 | | 参数 | 类型 | 说明 | 必填 | 缺省 | 797 | | ------- | ----------------------- | ------------------ | :--: | :--: | 798 | | handle | string\|function\|table | 扩展组件主体内容 | 是 | | 799 | | options | any | 透传给扩展组件函数 | 否 | | 800 | 801 | 802 | 803 | #### handle 804 | 805 | **string**: 806 | 807 | 包名字符串,如 `resty.tofu.extend.task` ,先查找应用extend目录下,如果没有再按`require`方式加载。 808 | 809 | ``` lua 810 | -- 配置文件 conf/extend.lua 811 | 812 | extend = { 813 | 814 | -- 快速配置方式一 815 | 'resty.tofu.extend.builtin', 816 | 817 | -- 快速配置方式二, 等同上面方式 818 | { 819 | default = { 820 | handle = 'resty.tofu.extend.builtin', 821 | } 822 | }, 823 | } 824 | ``` 825 | 826 | **function**: 827 | 828 | 函数, 有些比较简单或临时的扩展组件,我们可以直接写在配置文件中 829 | 830 | ``` lua 831 | -- 配置文件 conf/extend.lua 832 | 833 | extend = { 834 | 835 | { 836 | named = 'bye' 837 | handle = function (options) 838 | return function (who) 839 | tofu.log.d(who, ': 散水,系噉先喇!') 840 | end 841 | end, 842 | options = { } 843 | }, 844 | } 845 | 846 | -- 在其它地方调用 tofu.bye('肥标') 847 | ``` 848 | 849 | **table**: 850 | 851 | 将`handle`所有非下划线`_`开头的属性或方法合并(浅copy)到`tofu`中 852 | 853 | ``` lua 854 | -- 配置文件 conf/extend.lua 855 | 856 | extend = { 857 | -- 情况1: 没有命名 858 | { 859 | -- named = nil 860 | handle = { 861 | bye = function (who) 862 | tofu.log.d(who, ': 散水,系噉先喇!') 863 | end, 864 | } 865 | }, 866 | -- 在其它地方调用 tofu.bye('肥标') 867 | 868 | 869 | -- 情况2: 指定命名 870 | { 871 | named = 'foo', 872 | handle = { 873 | -- 如果存在 new 方法则调用 874 | new = function(options) 875 | return { 876 | bye = function (who) 877 | tofu.log.d(who, ': 散水,系噉先喇!') 878 | end, 879 | } 880 | end 881 | } 882 | }, 883 | -- 在其它地方调用 tofu.foo.bye('肥标') 884 | } 885 | 886 | 887 | ``` 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | ## 内置中间件 896 | 897 | 898 | 899 | 900 | 901 | ### 访问请求追踪中间件 / trace 902 | 903 | > resty.tofu.middleware.trace 904 | 905 | 一个简单记录请求的进入中间件时间, 退出中间件时间,请求的uri 和 方法,状态,并使用指定的方法记录/打印 906 | 907 | #### 配置 options 908 | 909 | | 参数 | 类型 | 说明 | 必须 | 缺省 | 910 | | ------ | -------- | -------------- | :--: | :-----------: | 911 | | logger | function | 记录日志的方法 | 否 | ngx.log(INFO) | 912 | | | | | | | 913 | 914 | 915 | 916 | #### 使用配置 917 | 918 | ``` lua 919 | -- 配置文件 conf/middleware.lua 920 | 921 | middleware = { 922 | { 923 | handle = 'resty.touf.middleware.trace', 924 | options = { 925 | logger = tofu.log.n 926 | } 927 | }, 928 | } 929 | ``` 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | ### 路由中间件 / router 938 | 939 | > resty.tofu.middleware.router 940 | 941 | uri路由解析,uri参数解析, 解析结果存放在 context 中 942 | 943 | #### 配置 options 944 | 945 | | 参数 | 类型 | 说明 | 必须 | 缺省 | 946 | | ---------- | ------ | ---------------------- | :--: | :-----: | 947 | | module | string | 缺省的`module`名称 | 是 | default | 948 | | controller | string | 缺省的`controller`名称 | 是 | index | 949 | | action | string | 缺省的`action`名称 | 是 | index | 950 | 951 | 952 | 953 | #### 使用配置 954 | 955 | ``` lua 956 | -- 配置文件 conf/middleware.lua 957 | 958 | middleware = { 959 | { 960 | handle = 'resty.touf.middleware.router', 961 | options = { 962 | module = 'default', 963 | controller = 'index', 964 | action = 'index' 965 | } 966 | }, 967 | } 968 | ``` 969 | 970 | 971 | 972 | #### 路由解析规则 973 | 974 | 通过解析 ngx.var.uri 为 `[/module]` `[/.../path/../]` `[/controller]` `[/action]` `[/arg1/arg2.../...]` 975 | 976 | [] 中的内容为可选, 如果module, controller, action 各项为空时,则使用配置 977 | 978 | 1. 使用 `/` 分割 uri 979 | 2. 从左向右查找和检验`module`, 如果没有则使用 `options.module` 配置值 980 | 3. 在`module`之后,从左向右查找和检验`controller`, 如果没有则使用 `options.controller` 配置值 981 | 4. 在`controller`之后,从左向右查找和检验`action`, 如果没有则使用 `options.action` 配置值 982 | 5. 在 `action`之后,从左向右的所有内容解析为 args 数组参数 983 | 984 | #### 解析结果合并到中间件 context 985 | 986 | | 参数 | 类型 | 说明 | 样例 | 987 | | ---------- | ------ | ---------------------------------------------- | ---------------- | 988 | | handler | string | 处理品的文件,相对于应用内的 controller/ 目录下 | default/index | 989 | | module | string | 模块名称 | default | 990 | | controller | string | 控制器名称 | index | 991 | | action | string | 方法名称 | index | 992 | | args | table | 参数表,数组 | {'arg1', 'arg2'} | 993 | 994 | 995 | 996 | 997 | 998 | ### 参数解析中间件 / payload 999 | 1000 | > resty.tofu.middleware.payload 1001 | 1002 | 解析处理 body 与 uri 参数,get参数合并到 context.args.get, 其它方式合并到 context.args.post, 文件合并到 context.args.file。 1003 | 1004 | 1005 | 1006 | #### 配置 options 1007 | 1008 | | 参数 | 类型 | 说明 | 必须 | 缺省 | 1009 | | ------ | ----- | ------------ | :--: | :--: | 1010 | | parser | table | 协议解析器表 | 否 | | 1011 | 1012 | > 默认支持 1013 | > 1014 | > application/x-www-form-urlencoded 1015 | > 1016 | > application/json 1017 | > 1018 | > multipart/form-data 1019 | 1020 | 1021 | 1022 | #### 使用配置 1023 | 1024 | ``` lua 1025 | -- 配置文件 conf/middleware.lua 1026 | 1027 | middleware = { 1028 | 'resty.tofu.middleware.payload', 1029 | } 1030 | ``` 1031 | 1032 | 1033 | 1034 | #### 解析器格式 1035 | 1036 | ``` lua 1037 | function() 1038 | return { post={}, get={}, file={} } 1039 | end 1040 | ``` 1041 | 1042 | > 结果会合并到中间件 context.args 中 1043 | 1044 | 1045 | 1046 | #### 添加或修改解析器 1047 | 1048 | ``` lua 1049 | -- 配置文件 conf/middleware.lua 1050 | 1051 | middleware = { 1052 | { 1053 | handle = 'resty.tofu.middleware.payload', 1054 | options = { 1055 | 1056 | -- 添加一个 xml 解析器 1057 | ['application/xml'] = function() 1058 | -- 按约定存放到 post | get | file 中 1059 | return { post = {} } 1060 | end, 1061 | 1062 | 1063 | -- 重写(覆盖) multipart/form-data 解析器 1064 | ['multipart/form-data'] = function() 1065 | return { post={}, file={} } 1066 | end, 1067 | 1068 | -- ... 1069 | }, 1070 | } 1071 | } 1072 | ``` 1073 | 1074 | > 建议,把解析器写成独文件,通过require引入,这样使得配置文件不至于冗长 1075 | 1076 | 1077 | 1078 | 1079 | 1080 | ### 参数过滤中间件 / guard 1081 | 1082 | > resty.tofu.middleware.guard 1083 | > 1084 | > 依赖: resty.tofu.middleware.router 1085 | 1086 | 参数过滤校检是一个不可少的环节,将参数统一处理,当参数校验完成后,再进行后续的流程。这样可以减少action 复杂且冗长和重复的代码,使得action更加注重业务方面 1087 | 1088 | 1089 | 1090 | #### 配置 options 1091 | 1092 | | 参数 | 类型 | 说明 | 必须 | 缺省 | 1093 | | ---------- | ------ | -------------------------- | :--: | :---: | 1094 | | guard_root | string | guard 起始查找目录 | 是 | guard | 1095 | | suffix | string | 方法后缀,如 action_fuffix | 是 | | 1096 | 1097 | 1098 | 1099 | #### 使用配置 1100 | 1101 | ``` lua 1102 | -- 配置文件 conf/middleware.lua 1103 | 1104 | middleware = { 1105 | 'resty.tofu.middleware.guard', 1106 | } 1107 | ``` 1108 | 1109 | > 使用默认配置基本可以满足需要 1110 | 1111 | 1112 | 1113 | #### 添加参数检验过滤器 1114 | 1115 | 当请求 `module/controller/action`, 中间件会在 guard 目录下查找 `module/controller/action` 。 1116 | 1117 | * 没有命中的guard,直接通过,执行后面的中间件流程 1118 | 1119 | * 命中guard中定义了_enter(魔术方法),会优先执行,并判断结果是否为非 false,否则中断流程 1120 | * 执行命中guard中定义的action方法,并判断结果是否为非 false 1121 | 1122 | 1123 | 1124 | 1125 | 1126 | 1127 | 1128 | ### 控制器中间件 / controller 1129 | 1130 | > resty.tofu.middleware.controller 1131 | > 1132 | > 依赖: resty.tofu.middleware.router 1133 | 1134 | 控制器是处理用户请求业务的主要场所,使得不同的业务对相应的`controller.action`简单明了。 1135 | 1136 | controller中间件根据路由中间件resty.tofu.middleware.router的处理结果,在 options.controller_root 配置的目录下开始查找,匹配对应用controller 1137 | 1138 | 1139 | 1140 | #### 配置 options 1141 | 1142 | | 参数 | 类型 | 说明 | 必须 | 缺省 | 1143 | | --------------------------- | -------- | ----------------------------- | :--: | :---------: | 1144 | | suffix | string | 方法action的后缀 | 否 | '' | 1145 | | logger | function | 方法后缀,如 action_fuffix | 否 | tofu.log.e | 1146 | | controller_root | string | 控制器查找的根目录 | 否 | controller/ | 1147 | | controller_allow_underscore | bool | 是否支持下划线`_`开头的控制器 | 否 | false | 1148 | | action_allow_underscore | bool | 是否支持下划线`_`开头的方法 | 否 | false | 1149 | 1150 | 1151 | 1152 | #### 使用配置 1153 | 1154 | ``` lua 1155 | -- 配置文件 conf/middleware.lua 1156 | 1157 | middleware = { 1158 | 'resty.tofu.middleware.controller', 1159 | } 1160 | ``` 1161 | 1162 | 1163 | 1164 | #### 如何编写 controller 1165 | 1166 | 默认情况下controller中间件会从<应用>/lua/controller/ 目录按 module/path/controller.lua 规则查找文件。 1167 | 1168 | 我们可以使用 lua module 的方式/风格(推荐)来编写 1169 | 1170 | ``` lua 1171 | -- 1172 | -- 文件 <应用>/lua/controller/api/user.lua 1173 | -- 1174 | 1175 | local _M = {} 1176 | 1177 | -- 缺省方法 index 1178 | function _M.index() 1179 | tofu.success() 1180 | end 1181 | 1182 | 1183 | -- 定义方法 info 1184 | -- 访问 /api/user/info 该方法将被调用 1185 | -- 请求参数 1186 | -- id : 用户id 1187 | -- 1188 | function _M.info() 1189 | -- 业务样例代码 1190 | -- -------------------------- 1191 | -- 获取参数 1192 | local id = tofu.args('id') 1193 | 1194 | -- 从数据库中查询用户信息 1195 | local info = tofu.model('user').get({id = id}) 1196 | 1197 | -- 以json方式反回结果 {errno:0, errmsg:'', data:{id:, name:, ...}} 1198 | tofu.success(info) 1199 | end 1200 | 1201 | return _M 1202 | ``` 1203 | 1204 | 1205 | 1206 | 使用controller基类是一个很好的方案 1207 | 1208 | ``` lua 1209 | -- 按习惯,私有的把它命名为 _base.lua 1210 | -- 1211 | local _M = {} 1212 | -- ... 方法 ... 1213 | return _M 1214 | ``` 1215 | 1216 | 1217 | 1218 | ``` lua 1219 | -- 这个controller 将"继承"上面的 _base 1220 | 1221 | local _M = tofu.extends('_base') -- tofu.extends 提供相对于当前文件目录的查找方式 1222 | -- .. 继承 _base 的方法.. 1223 | return _M 1224 | ``` 1225 | 1226 | 1227 | 1228 | #### 魔术方法 1229 | 1230 | * `_enter()` 当进入`controller` 时自动调用 1231 | * `_leave()` 当退出`controller` 时自动调用 1232 | 1233 | 1234 | 1235 | #### 中断流程 1236 | 1237 | 当` controller` 的方法明确 `return false` 时,会中断后面的中间件 1238 | 1239 | 1240 | 1241 | 1242 | 1243 | ## 内置扩展组件 / extend 1244 | 1245 | 1246 | 1247 | 1248 | 1249 | ### 配置扩展组件 / config 1250 | 1251 | > resty.tofu.extend.config 1252 | 1253 | 使用lua语法作为配置文件,一体化,无需额外学习。借助lua的解析能力,代码即配置,轻松提供强大且可扩展的配置方案。按顺序覆盖,根据环境使用不同配置。 1254 | 1255 | 1256 | 1257 | #### 配置 options 1258 | 1259 | | 参数 | 类型 | 说明 | 必须 | 缺省 | 1260 | | ------- | ------ | ---------------- | :--: | :------: | 1261 | | env | string | 当前环境 | 否 | 当前环境 | 1262 | | prefix | string | 配置查找目录 | 否 | conf/ | 1263 | | default | string | 默认配置文件名称 | 否 | config | 1264 | 1265 | 1266 | 1267 | #### 使用配置 1268 | 1269 | ``` lua 1270 | -- 配置文件 conf/extend.lua 1271 | 1272 | extend = { 1273 | { 1274 | named = 'config', 1275 | default = { 1276 | handle = 'resty.tofu.extend.config', 1277 | options = { 1278 | env = tofu.env, 1279 | prefix = tofu.ROOT_PATH .. 'conf/' 1280 | } 1281 | } 1282 | } 1283 | } 1284 | ``` 1285 | 1286 | 1287 | 1288 | #### 添加自定义配置 1289 | 1290 | 配置文件在`options.prefix`(默认 <应用>/conf/)目录下添加, 如有 `jwt` 配置 1291 | 1292 | ``` lua 1293 | -- jwt.lua 1294 | 1295 | jwt = { 1296 | secret = '5df7f1701b778d03d57456afea567922', 1297 | passthrough = true, 1298 | } 1299 | ``` 1300 | 1301 | 1302 | 1303 | #### 使用 1304 | 1305 | ``` lua 1306 | local jwt = tofu.config.jwt 1307 | tofu.log.d( jwt.secret ) 1308 | ``` 1309 | 1310 | > `tofu.config.jwt` 按顺序合并和覆盖 config.lua <-- config.环境.lua <-- jwt.lua <-- jwt.环境.lua 1311 | 1312 | 1313 | 1314 | #### 合并和覆盖 1315 | 1316 | ``` lua 1317 | -- 配置文件 config.lua 1318 | jwt = { 1319 | secret = '5df7f1701b778d03d57456afea567922' 1320 | } 1321 | 1322 | -- 配置文件 jwt.lua 1323 | jwt = { 1324 | secret = 'abc123' 1325 | } 1326 | 1327 | -- 配置文件 jwt.production.lua 1328 | jwt = { 1329 | passthrough = true 1330 | } 1331 | 1332 | ------------------------------------------- 1333 | -- jwt配置的结果 1334 | -- 1335 | jwt = { 1336 | secret = 'abc123', 1337 | passthrough = 1338 | } 1339 | 1340 | ``` 1341 | 1342 | 1343 | 1344 | 1345 | 1346 | ### 日志扩展组件 / log 1347 | 1348 | > resty.tofu.extend.log 1349 | 1350 | 提供了 debug, info, notice, warn, error,等多个级别日志,输出方式提供了终端和文件方式 1351 | 1352 | 1353 | 1354 | #### 配置 options 1355 | 1356 | | 参数 | 类型 | 说明 | 必须 | 缺省 | 1357 | | ------- | -------- | --------------------------------------------- | :--: | :---: | 1358 | | level | int | 记录日志级别,低于设置级别会被忽略 | 否 | DEBUG | 1359 | | color | bool | 是否使用颜色,一般在终端环境时使用 | 否 | false | 1360 | | colors | table | 配色表 | 否 | | 1361 | | printer | function | 记录器 | 否 | | 1362 | | fmter | string | 日志格式化模板 | 否 | | 1363 | | pretty | bool | 是否格式化输出 | 否 | false | 1364 | | trace | bool | 是否显示更详细信息(文件名,行号等) | 否 | false | 1365 | | file | string | file日志记录器配置 | 否 | | 1366 | | rotate | string | file日志记录器配置, 切割间隔。目前只支持'day' | 否 | | 1367 | 1368 | 1369 | 1370 | #### 使用配置 1371 | 1372 | ``` lua 1373 | -- 配置文件 conf/extend.lua 1374 | 1375 | local _log = require 'resty.tofu.extend.log' 1376 | local _isdev = 'development' == tofu.env 1377 | 1378 | extend = { 1379 | { 1380 | named = 'log', 1381 | type = _isdev and 'console' or 'file' 1382 | 1383 | -- 终端日志 1384 | console = { 1385 | handle = _log.console, 1386 | options = { 1387 | level = _log.levels.DEBUG, 1388 | color = true, 1389 | pretty = true, 1390 | } 1391 | }, 1392 | 1393 | -- 文件日志 1394 | file = { 1395 | handle = _log.file, 1396 | options = { 1397 | level = _log.levels.INFO, 1398 | file = 'logos/tofu.log', 1399 | rotate = 'day', 1400 | } 1401 | } 1402 | } 1403 | } 1404 | ``` 1405 | 1406 | 1407 | 1408 | #### Lua API 1409 | 1410 | **tofu.log.d**(...) 1411 | 1412 | debug 级别日志 1413 | 1414 | 1415 | 1416 | **tofu.log.i**(...) 1417 | 1418 | info级别日志 1419 | 1420 | 1421 | 1422 | **tofu.log.n**(...) 1423 | 1424 | notice级别日志 1425 | 1426 | 1427 | 1428 | **tofu.log.w**(...) 1429 | 1430 | warn级别日志 1431 | 1432 | 1433 | 1434 | **tofu.log.e**(...) 1435 | 1436 | error级别日志 1437 | 1438 | 1439 | 1440 | ``` lua 1441 | tofu.log.d('----- debug -----') 1442 | tofu.log.i('----- info -----') 1443 | tofu.log.n('----- notice -----') 1444 | tofu.log.w('----- warn -----') 1445 | tofu.log.e('----- error -----') 1446 | ``` 1447 | 1448 | 1449 | 1450 | 1451 | 1452 | ### session 扩展组件 /session 1453 | 1454 | > resty.tofu.extend.session 1455 | > 1456 | > 依赖: [lua-resty-session](https://github.com/bungle/lua-resty-session) 1457 | 1458 | #### 配置 options 1459 | 1460 | | 参数 | 类型 | 说明 | 必须 | 缺省 | 1461 | | ------- | ------ | --------------------------------------------------- | :--: | :----------: | 1462 | | name | string | session名称设置 | 否 | tofu_session | 1463 | | ttl | int | session有效时长(秒) | 否 | 1200 | 1464 | | renewal | bool | 是否自动续约ttl | 否 | true | 1465 | | secret | string | session安全码 | 否 | | 1466 | | storage | string | session的存储方式,默认支持 cookie \| shm 等多种方式 | 否 | | 1467 | 1468 | 1469 | 1470 | #### 使用配置 1471 | 1472 | ``` lua 1473 | -- 配置文件 conf/extend.lua 1474 | 1475 | extend = { 1476 | { 1477 | named = 'session', 1478 | default = { 1479 | handle = 'resty.tofu.extend.session', 1480 | options = { 1481 | ttl = 20 * 60, -- 过期时间(秒) 1482 | 1483 | -- 与 tofu.nginx.conf 中的 set $session_secret 相同 1484 | secret = '5df7f1701b778d03d57456afea567922', 1485 | 1486 | -- -- cookie 方式 1487 | -- storage = 'cookie' 1488 | -- cookie = {} 1489 | 1490 | -- -- shm 方式 1491 | storage = 'shm', 1492 | shm = { 1493 | -- 匹配 tofu.nginx.conf 中的 lua_shared_dict 1494 | store = 'tofu_sessions' 1495 | } 1496 | } 1497 | } 1498 | } 1499 | 1500 | } 1501 | ``` 1502 | 1503 | 1504 | 1505 | ``` nginx 1506 | ##  conf/tofu.nginx.conf 1507 | 1508 | http { 1509 | ## ... 1510 | ## 上 conf/extend.lua 中 session 配置的shm store 名称相配 1511 | lua_shared_dict tofu_sessions 10m; 1512 | 1513 | ## ... 1514 | } 1515 | ``` 1516 | 1517 | 1518 | 1519 | #### Lua API 1520 | 1521 | **tofu.session.get(key)** 1522 | 1523 | 获取当前session的key的值 1524 | 1525 | ``` lua 1526 | local info = tofu.session.get('info') 1527 | ``` 1528 | 1529 | 1530 | 1531 | **tofu.session.set(key, value)** 1532 | 1533 | 设置当前session的key的值 , 并返回旧的值 1534 | 1535 | ``` lua 1536 | tofu.session.set('info', {id=1, name='d'}) 1537 | ``` 1538 | 1539 | 1540 | 1541 | **tofu.session.destroy()** 1542 | 1543 | 删除当前session的所有值 1544 | 1545 | ``` lua 1546 | tofu.session.destroy() 1547 | ``` 1548 | 1549 | 1550 | 1551 | 1552 | 1553 | ### cache 扩展组件 / cache 1554 | 1555 | > resty.tofu.extend.cache 1556 | 1557 | 使用ngx.shared.DICT作为缓存,不支持table, userdata类型 1558 | 1559 | 1560 | 1561 | #### 配置 options 1562 | 1563 | | 参数 | 类型 | 说明 | 必须 | 缺省 | 1564 | | ---- | ------ | ------------------- | :--: | :-------------: | 1565 | | ttl | int | 缓存时间(秒) | 是 | 5400 | 1566 | | shm | string | lua_shared_dice名称 | 是 | tofu_cache_dict | 1567 | 1568 | 1569 | 1570 | #### 使用配置 1571 | 1572 | ``` lua 1573 | -- conf/extend.lua 1574 | 1575 | extend = { 1576 | { 1577 | named = 'cache', 1578 | default = { 1579 | handle = 'resty.tofu.extend.cache', 1580 | options = { 1581 | ttl = 90 * 60, -- 90 分钟 1582 | shm = 'tofu_cache_dict', -- lua_shared_dict 配置的名称 1583 | } 1584 | } 1585 | } 1586 | } 1587 | ``` 1588 | 1589 | 1590 | 1591 | #### Lua API 1592 | 1593 | **tofu.cache.get(key [, init] [, ...])** 1594 | 1595 | 获得缓存,如果不存在则返回 init, 如果init 是 function,则执行 init(...), 只有值为非nil才会缓存。 1596 | 1597 | 初始化时会使用 resty.lock, 所以不会有缓存失效风暴 1598 | 1599 | 1600 | 1601 | **tofu.cache.set(key, val [, ttl])** 1602 | 1603 | 设置缓存, val 不支持 table 1604 | 1605 | 1606 | 1607 | **tofu.cache.del(key)** 1608 | 1609 | 删除缓存 1610 | 1611 | 1612 | 1613 | **tofu.cache.incr(key, val [, init] [, ttl])** 1614 | 1615 | 累加器 1616 | 1617 | 1618 | 1619 | ### 视图扩展组件 / view 1620 | 1621 | > resty.tofu.extend.view 1622 | > 1623 | > 依赖: [ lua-resty-template ]([bungle/lua-resty-template: Templating Engine (HTML) for Lua and OpenResty. (github.com)](https://github.com/bungle/lua-resty-template)) 1624 | 1625 | 通过开启视图扩展组件,让应用有服务端渲染模板的能力。 1626 | 1627 | 1628 | 1629 | #### 配置 options 1630 | 1631 | | 参数 | 类型 | 说明 | 必须 | 缺省 | 1632 | | ------------- | ------ | ------------ | :--: | :---: | 1633 | | template_root | string | 模板搜索路径 | 否 | view/ | 1634 | | extname | string | 模板后缀名称 | 否 | .html | 1635 | | cache | bool | 是否缓存 | 否 | false | 1636 | 1637 | 1638 | 1639 | #### 使用配置 1640 | 1641 | ``` lua 1642 | -- conf/extend.lua 1643 | 1644 | extend = { 1645 | { 1646 | named = 'view', 1647 | default = { 1648 | handle = 'resty.tofu.extend.view', 1649 | options = { 1650 | template_root = tofu.ROOT_PATH .. 'view/', 1651 | extname = '.html', 1652 | cache = true, 1653 | } 1654 | } 1655 | } 1656 | } 1657 | ``` 1658 | 1659 | 1660 | 1661 | #### Lua API 1662 | 1663 | **tofu.view.assign(param [, val])** 1664 | 1665 | 关联变量到模板中使用 1666 | 1667 | ``` lua 1668 | -- 方式一 1669 | tofu.view.assign('name', '神探肥标') 1670 | tofu.view.assign('id', 123) 1671 | 1672 | -- 方式二, 与上面等效 1673 | tofu.view.assign({ 1674 | name = '神探肥标', 1675 | id = 123 1676 | }) 1677 | ``` 1678 | 1679 | 1680 | 1681 | **tofu.view.render(tpl, param)** 1682 | 1683 | 渲染模板 1684 | 1685 | 1686 | 1687 | **tofu.view.display(tpl, param)** 1688 | 1689 | param 合并 assign所关联变量,并渲染模板到body 1690 | 1691 | ``` lua 1692 | -- 方式一 1693 | tofu.view.assign('name', '神探肥标') 1694 | tofu.view.assign('id', 123) 1695 | tofu.view.display() 1696 | 1697 | -- 方式二, 可以使用链式风格 1698 | tofu.view 1699 | .assign('name', '神探肥标') 1700 | .assign('id', 123) 1701 | .assign({ 1702 | pic = '9527' 1703 | }) 1704 | .display() 1705 | 1706 | ``` 1707 | 1708 | 1709 | 1710 | 1711 | 1712 | ### mysql 访问扩展组件 / model 1713 | 1714 | > resty.tofu.extend.model 1715 | 1716 | mysql 是一个较常见也很好用的关系弄数据,在开发中使用SQL操作数据库(CRUD: 增删改查),还是比较麻烦,也容易造成 SQL 注入等安全问题。model 扩展组件提供了快速执行sql,和模型功能。 1717 | 1718 | 1719 | 1720 | #### 配置 options 1721 | 1722 | | 参数 | 类型 | 说明 | 必须 | 缺省 | 1723 | | ------- | --------------------------- | ------------------------------ | :--: | :--: | 1724 | | default | string \| table \| function | 指定缺省时的数据库options 配置 | 是 | | 1725 | 1726 | **数据库配置 options** 1727 | 1728 | | 参数 | 类型 | 说明 | 必须 | 缺省 | 1729 | | ---------------- | ------ | -------------------------- | :--: | :-------: | 1730 | | host | string | 数据库地址(tcp/ip) | 否 | 127.0.0.1 | 1731 | | port | int | 数据库端口 | 否 | 3306 | 1732 | | path | string | 数据库地址(unix socket) | 否 | | 1733 | | database | string | 数据库名称 | 否 | | 1734 | | prefix | string | 数据表前缀 | 否 | '' | 1735 | | user | string | 数据库用户 | 否 | root | 1736 | | password | string | 数据库密码 | 否 | '' | 1737 | | charset | string | 连接编码 | 否 | utf8 | 1738 | | timeout | int | 连接超时(毫秒) | 否 | 5000 | 1739 | | max_package_size | int | 最大结果集大小 | 否 | 2M | 1740 | | pool_size | int | 连接池大小(每nginx worker) | 否 | 64 | 1741 | | logconnect | bool | 是否log连接 | 否 | false | 1742 | | logsql | bool | 是否log sql 语记 | 否 | false | 1743 | 1744 | 1745 | 1746 | #### 使用配置 1747 | 1748 | ``` lua 1749 | -- conf/extend.lua 1750 | 1751 | local _isdev = 'development' == tofu.env 1752 | 1753 | extend = { 1754 | { 1755 | named = 'model', 1756 | default = { 1757 | handle = 'resty.tofu.extend.model', 1758 | options = { 1759 | host = '127.0.0.1', 1760 | user = 'root', 1761 | password = '', 1762 | database = 'test', 1763 | 1764 | logconnect = _isdev, 1765 | logsql = _iddev, 1766 | } 1767 | } 1768 | } 1769 | } 1770 | ``` 1771 | 1772 | 1773 | 1774 | **多数据库配置** 1775 | 1776 | ``` lua 1777 | -- conf/extend.lua 1778 | 1779 | local _isdev = 'development' == tofu.env 1780 | 1781 | extend = { 1782 | { 1783 | named = 'model', 1784 | default = { 1785 | handle = 'resty.tofu.extend.model', 1786 | default = 'db1', -- 指定默认使用的数据配置 1787 | -- 名为db1的数据库配置 1788 | db1 = { 1789 | host = '192.168.0.1', 1790 | user = 'root', 1791 | password = '', 1792 | database = 'test', 1793 | logconnect = _isdev, 1794 | logsql = _iddev, 1795 | }, 1796 | 1797 | -- 名为db2的数据库配置 1798 | db2 = { 1799 | host = '192.168.0.2', 1800 | user = 'root', 1801 | password = '', 1802 | database = 'test', 1803 | logconnect = _isdev, 1804 | logsql = _iddev, 1805 | }, 1806 | 1807 | -- dbN 1808 | -- ... 1809 | } 1810 | } 1811 | } 1812 | ``` 1813 | 1814 | 1815 | 1816 | **运行时动态获取,如读写分离** 1817 | 1818 | ``` lua 1819 | -- conf/extend.lua 1820 | 1821 | local _match = string.match 1822 | local _lower = string.lower 1823 | 1824 | extend = { 1825 | { 1826 | named = 'model', 1827 | default = { 1828 | handle = 'resty.tofu.extend.model', 1829 | 1830 | -- 样例:根据sql,使用选择数据库配置 1831 | default = function (sql) 1832 | local op = _match(sql, '^%s-(%a+)') 1833 | op = _lower(op) 1834 | 1835 | -- 只读数据库 1836 | if 'select' == op then 1837 | return { 1838 | host = '192.168.0.1', 1839 | user = 'root', 1840 | password = '', 1841 | database = 'test', 1842 | logconnect = _isdev, 1843 | logsql = _iddev, 1844 | } 1845 | 1846 | -- 读写数据库 1847 | else 1848 | return { 1849 | host = '192.168.0.2', 1850 | user = 'root', 1851 | password = '', 1852 | database = 'test', 1853 | logconnect = _isdev, 1854 | logsql = _iddev, 1855 | } 1856 | end 1857 | } 1858 | } 1859 | } 1860 | ``` 1861 | 1862 | 1863 | 1864 | #### Lua API 1865 | 1866 | **tofu.model(name [ , opts])** 1867 | 1868 | 创建一个 model , 对于单表操作, 这是个利器,可以很方便地完成较复杂的操作。也可以直接执行sql语句。 1869 | 1870 | `name` string 通常是表名, 先尝试加载`<应用>/lua/model/.lua`,然后创建一个model。 1871 | 1872 | `opts` string | table | function 可选参,数据库配置 1873 | 1874 | ``` lua 1875 | -- 创建一个model 1876 | 1877 | -- 使用默认配置 1878 | local user = tofu.model('user') 1879 | 1880 | -- 使用配置(遵循覆盖原则) 1881 | local user = tofu.model('user', { host='127.0.0.1' }) 1882 | 1883 | -- 使用名为 db2 的配置 1884 | local user = tofu.model('user', 'db2') 1885 | 1886 | -- 使用一个函数的结果作为配置 1887 | -- function(sql) -> table 1888 | local user = tofu.model('user', function(sql) return { host='127.0.0.1' } end ) 1889 | 1890 | ``` 1891 | 1892 | 1893 | 1894 | **model.exec(sql [, opts])** 1895 | 1896 | 使用 model 执行sql, 提供灵活的占位符操作 1897 | 1898 | `sql` string | table 1899 | 1900 | * **string 合法的sql语记** 1901 | 1902 | ``` lua 1903 | local sql = 'select * from user limit 10' 1904 | local result = tofu.model().exec(sql) 1905 | ``` 1906 | 1907 | * **table {sql, param}** 1908 | 1909 | **{string, ...}, 占位符 ?**,安全占位符会根据参数类型,进行转换 1910 | 1911 | ``` lua 1912 | local sql = 'select * from user where id=? and name=?' 1913 | tofu.model().exec({sql, 123, '神探肥标'}) 1914 | 1915 | -- 执行的sql: select * from user where id=123 and name='神探肥标' 1916 | ``` 1917 | 1918 | 1919 | 1920 | **{string, ...}, 占位符 ??**,==不安全占位符,不进行安全转换== 1921 | 1922 | ``` lua 1923 | local sql = 'select ?? from user where id=?' 1924 | tofu.model().exec({sql, 'name', 123}) 1925 | 1926 | -- 执行的sql: select name from user where id=123 1927 | ``` 1928 | 1929 | 1930 | 1931 | **{string, {...}}, 有名占位符 :name**,安全占位符会根据参数类型,进行转换 1932 | 1933 | ``` lua 1934 | local sql = 'select * from user where id=:id and name=:name' 1935 | tofu.model().exec({sql, { id=123, name='神探肥标'}) 1936 | 1937 | -- 执行的sql: select * from user where id=123 and name='神探肥标' 1938 | ``` 1939 | 1940 | 1941 | 1942 | **{string, ...}, 有名占位符 ::name**,==不安全占位符,不进行安全转换== 1943 | 1944 | ``` lua 1945 | local sql = 'select ::field from user where id=:id' 1946 | tofu.model().exec({sql, { field='id, name', id=123}) 1947 | 1948 | -- 执行的sql: select id, name from user where id=123 1949 | ``` 1950 | 1951 | `opts` table 可选参,数据库配置 1952 | 1953 | 1954 | 1955 | **model.get(cond, opts)** 1956 | 1957 | 获取数据,默认只获取一条记录 1958 | 1959 | `cond` string | table 1960 | 1961 | sql 语句 where 后面的内容,支持 `?` | `??` | `:name` | `::name` 占位符格式。 可参考上面model.exec的样例 1962 | 1963 | * string 1964 | 1965 | ``` lua 1966 | 1967 | local result = tofu.model('employees').get('emp_id=123') 1968 | -- 执行的sql: select * from employees where emp_id=123 1969 | ``` 1970 | 1971 | * table 1972 | 1973 | ``` lua 1974 | -- 占位符 ? 1975 | local result = tofu.model('employees').get({'emp_id=?', '123'}) 1976 | -- 执行的sql: select * from employees where emp_id='123' 1977 | 1978 | 1979 | -- 占位符 ?? 1980 | local result = tofu.model('employees').get({'emp_id=??', '123'}) 1981 | -- 执行的sql: select * from employees where emp_id=123 1982 | 1983 | -- 有名占位符 :name 与 :name 同理 1984 | ``` 1985 | 1986 | 1987 | 1988 | 改变关系逻辑符, 逻辑符使用 cond[1] 控制, ==不作安全转换==,也就是任何内容都会原样插入 1989 | 1990 | ``` lua 1991 | -- 默认情况以 and 连接多个条件 1992 | tofu.model('employees').get({emp_id=123, name='神探肥标'}) 1993 | -- 执行的sql: select * from employees where emp_id=123 and name='神探肥标' 1994 | 1995 | -- 使用 or 连接 1996 | tofu.model('employees').get({'or', emp_id=123, name='神探肥标'}) 1997 | -- 执行的sql: select * from employees where emp_id=123 or name='神探肥标' 1998 | ``` 1999 | 2000 | 当值为table即 key = { op, val, ...}, `op`为操作符, ==不作安全转换==, `>` | `<` | `=` | `between` | `in` | `not in`等等合法的sql语法都支持。 2001 | 2002 | 下面只列举出model中特有的操作符 2003 | 2004 | | 操作符 | 说明 | 关系 | 2005 | | ------ | ------------ | :-----------------------: | 2006 | | [] | 闭区间 | [a,b] = {x \| a ≤ x ≤ b} | 2007 | | () | 开区间 | (a,b) = {x \| a < x < b} | 2008 | | [) | 半开半闭区间 | [a,b) = {x \| a ≤ x < b} | 2009 | | (] | 半开半闭区间 | (a,b] = {x \| a < x ≤ b} | 2010 | 2011 | 样例 2012 | 2013 | ``` lua 2014 | -- 为减少内容影响,下面model.get调用 opts 参数: 2015 | local opts = { limit = false } 2016 | 2017 | -- select * from employees where emp_id < 123 2018 | tofu.model('employees').get( {emp_id = {'<', 123} }, opts ) 2019 | 2020 | -- select * from employees where emp_id <= 123 2021 | tofu.model('employees').get( {emp_id = {'<=', 123} }, opts ) 2022 | 2023 | -- select * from employees where emp_id > 123 2024 | tofu.model('employees').get( {emp_id = {'>', 123} }, opts ) 2025 | 2026 | -- select * from employees where emp_id >= 123 2027 | tofu.model('employees').get( {emp_id = {'>=', 123} }, opts ) 2028 | 2029 | -- select * from employees where emp_id is null 2030 | tofu.model('employees').get( {emp_id = {'is null'} }, opts ) 2031 | 2032 | -- select * from employees where emp_id is not null 2033 | tofu.model('employees').get( {emp_id = {'is not null'} }, opts ) 2034 | 2035 | -- select * from `employees` where `emp_id` between 1 and 20 2036 | tofu.model('employees').get( {emp_id = {'between', 1, 20 } }, opts ) 2037 | 2038 | -- select * from `employees` where `emp_id` in (1,20) 2039 | tofu.model('employees').get( {emp_id = {'in', 1, 20 } }, opts ) 2040 | 2041 | -- select * from `employees` where `emp_id` not in (1,20) 2042 | tofu.model('employees').get( {emp_id = {'not in', 1, 20 } }, opts ) 2043 | 2044 | -- 2045 | -- 区间操作符 2046 | -- 2047 | -- select * from `employees` where 1 <= `emp_id` and `emp_id` <= 20 2048 | tofu.model('employees').get( {emp_id = {'[]', 1, 20 } }, opts ) 2049 | 2050 | -- select * from `employees` where 1 < `emp_id` and `emp_id` < 20 2051 | tofu.model('employees').get( {emp_id = {'()', 1, 20 } }, opts ) 2052 | 2053 | -- select * from `employees` where 1 <= `emp_id` and `emp_id` < 20 2054 | tofu.model('employees').get( {emp_id = {'[)', 1, 20 } }, opts ) 2055 | 2056 | -- select * from `employees` where 1 < `emp_id` and `emp_id` <= 20 2057 | tofu.model('employees').get( {emp_id = {'(]', 1, 20 } }, opts ) 2058 | ``` 2059 | 2060 | 2061 | 2062 | `opts` table 可选项 2063 | 2064 | | 参数 | 类型 | 说明 | 缺省 | 2065 | | --------- | ------------------------ | --------------------------------- | :---: | 2066 | | filed | string \| table | 对应sql中的字段 | * | 2067 | | limit | number \| table \| false | 对应sql中的limit, false:关闭limit | 1 | 2068 | | orderby | string | 对应sql中的 order by | | 2069 | | forupdate | bool | 对应sql中的 for update | false | 2070 | 2071 | 2072 | 2073 | model.set(cond, pair, add) 2074 | 2075 | 更新/添加数据 2076 | 2077 | `cond` string | table 2078 | 2079 | 参考上面model.get的样例。 2080 | 2081 | `pair` table 2082 | 2083 | 需要更新或添加数据对, kv 结构,k为字段名称要求合乎sql规则,v可以tostring类型, 如果v是table 且table[1] 是 string,会按其值原样处理,==不作安全转换== 2084 | 2085 | `add` bool | table 2086 | 2087 | 添加模式:将 cond 与 add 合并,进行添加操作, 如果存在则使用 pair 进行更新。所以要求 cond 与 add 的key与value 必须是合乎sql要的 2088 | 2089 | return 2090 | 2091 | * 更新模式: 受影响数量 2092 | 2093 | * 添加模式: 受影响数量, N 添加的id号, 0:更新。(mysql 的 @@innodb_autoinc_lock_mode 设置会影响结果) 2094 | 2095 | * 错误: nil, error 2096 | 2097 | 样例 2098 | 2099 | ``` lua 2100 | -- 2101 | -- 更新模式 2102 | -- 2103 | -- sql: update `employees` set `emp_name`='神探肥标' where emp_id = 1 2104 | tofu.model('employees').set({emp_id = 1}, {emp_name='神探肥标'}) 2105 | 2106 | -- sql: update `employees` set `salary`=salary+1 where emp_id = 1 2107 | tofu.model('employees').set({emp_id = 1}, {salary={'salary+1'} }) 2108 | 2109 | -- 2110 | -- 添加模式, 先进行添加,如果存在则更新 2111 | -- 2112 | -- sql: insert into `employees` (`emp_id`,`emp_name`) values (1,'蛋散') 2113 | -- on duplicate key update emp_name='神探肥标' 2114 | tofu.model('employees').set({emp_id = 1}, {emp_name='神探肥标'}, {emp_name='蛋散'}) 2115 | ``` 2116 | 2117 | 2118 | 2119 | model.add(pair [, ...]) 2120 | 2121 | 添加一条或多条数据 2122 | 2123 | `pair` table 2124 | 2125 | 数据对 kv 结构,k为字段名称要求合乎sql规则,v可以tostring类型, 如果v是table 且table[1] 是 string,会按其值原样处理,==不作安全转换==. 2126 | 2127 | 如果是添加多条,由第一条数据结构决定字段数量 2128 | 2129 | `return` 2130 | 2131 | * id 2132 | 2133 | > 当添加多条数据时,仅是第一条数据的id 2134 | 2135 | * nil, error 2136 | 2137 | ``` lua 2138 | -- sql: insert into `employees` (`emp_name`) values ('神探肥标') 2139 | tofu.model('employees').add({emp_name='神探肥标'}) 2140 | 2141 | -- sql: insert into `employees` (`emp_name`) values ('神探肥标'),('厨师废柴') 2142 | tofu.model('employees').add({emp_name='神探肥标'}, {emp_name='厨师废柴'}) 2143 | ``` 2144 | 2145 | 2146 | 2147 | model.del(cond) 2148 | 2149 | 删除数据 2150 | 2151 | `cond` string | table 2152 | 2153 | 参考上面model.get的cond参数。 2154 | 2155 | return 2156 | 2157 | * number 响应条数 2158 | * nil, error 2159 | 2160 | ``` lua 2161 | -- sql: delete from `employees` where `emp_id`=99 2162 | tofu.model('employees').del({emp_id=99}) 2163 | ``` 2164 | 2165 | 2166 | 2167 | model.begin([opts]) 2168 | 2169 | 主动开始事务 2170 | 2171 | `opts` 数据库配置 2172 | 2173 | 2174 | 2175 | model.rollback() 2176 | 2177 | 主动事件回滚 2178 | 2179 | 2180 | 2181 | model.commit() 2182 | 2183 | 主动事务提交 2184 | 2185 | ``` lua 2186 | -- 2187 | local m = tofu.model() 2188 | local ok = m.model('employees').add({emp_name = '神探肥标'}) 2189 | 2190 | if ok then 2191 | m.commit() 2192 | else 2193 | m.rollback() 2194 | end 2195 | 2196 | ``` 2197 | 2198 | 2199 | 2200 | model.trans(fn [, ...]) 2201 | 2202 | 事务处理,自动执行 begin commit / rollback 2203 | 2204 | `fn` function 2205 | 2206 | 业务处理函数 2207 | 2208 | `...` any 2209 | 2210 | 转递给 fn(...) 函数的参数 2211 | 2212 | `return` 2213 | 2214 | 返回 fn 的返回值 2215 | 2216 | ``` lua 2217 | local m = tofu.model() 2218 | local result = m.trans( function() 2219 | local list = m.model('employees').get(nil, {limit = 10}) 2220 | if #list < 1 then 2221 | return 2222 | end 2223 | -- 2224 | -- ... 业务处理 2225 | -- 2226 | local it = list[1] 2227 | return m.model('employees').del({emp_id = it.emp_id}) 2228 | end) 2229 | 2230 | -- 执行的 sql 2231 | -- BEGIN 2232 | -- select * from `employees` limit 10 2233 | -- ... 2234 | -- delete from `employees` where `emp_id`=21 2235 | -- COMMIT 2236 | -- 2237 | ``` 2238 | 2239 | 2240 | 2241 | 2242 | 2243 | ### 定时任务扩展组件 / task 2244 | 2245 | > resty.tofu.extend.task 2246 | 2247 | 该扩展组件熟于服务组件,提供了三种方式的定时任务 2248 | 2249 | #### 配置 options 2250 | 2251 | | 参数 | 类型 | 说明 | 必须 | 缺省 | 2252 | | ------ | --------------- | ---------------------------------------------- | :--: | :--------------: | 2253 | | worker | string \| int | 绑定到nginx的worker上执行任务 | 是 | privileged agent | 2254 | | task | string \| table | 任务表或名称, 如果是名称,则在加载conf/名称.lua | 是 | task | 2255 | 2256 | 2257 | 2258 | #### 使用配置 2259 | 2260 | ``` lua 2261 | -- 配置文件 conf/extend.lua 2262 | 2263 | extend = { 2264 | { 2265 | named = 'task', 2266 | default = { 2267 | handle = 'resty.touf.extend.task', 2268 | options = { 2269 | worker = 'privileged agent', 2270 | task = 'task', 2271 | } 2272 | } 2273 | }, 2274 | } 2275 | ``` 2276 | 2277 | 2278 | 2279 | #### task 任务配置 2280 | 2281 | | 参数 | 类型 | 说明 | 必须 | 缺省 | 2282 | | --------- | ------------------ | ---------------------------------------------------- | :--: | :---: | 2283 | | after | int | 延时执行定时器, 在设定的N秒后执行一次任务 | 否 | | 2284 | | interval | int | 间隔执行定时器,每隔设定的N秒执行一次任务 | 否 | | 2285 | | cron | string | 计划定时器, 格式 * * * * * 分别代表每 分 时 天 月 周 | 否 | | 2286 | | handle | string \| function | 要执行的函数或包名 | 否 | | 2287 | | enable | bool | 是否开启该任务 | 否 | true | 2288 | | immediate | bool | 是否立即执行一次 | 否 | false | 2289 | 2290 | task 配置格式 2291 | 2292 | ``` lua 2293 | -- conf/task.lua 2294 | 2295 | task = { 2296 | -- 延时执行定时器 2297 | { 2298 | after = 3, 2299 | handle = function() 2300 | tofu.log.d('系统启动后,等待3了秒,然后执行了一次') 2301 | end 2302 | }, 2303 | 2304 | -- 间隔执行定时器 2305 | { 2306 | interval = 5, 2307 | handle = function() 2308 | tofu.log.d('系统启动后,每隔5了秒,执行了一次') 2309 | end 2310 | }, 2311 | 2312 | -- 计划执行定时器 2313 | { 2314 | cron = '0 3 * * *', -- 每天在03:00时执行计划任务task.clean 2315 | handle = 'task.clean' 2316 | } 2317 | 2318 | } 2319 | ``` 2320 | 2321 | 2322 | 2323 | ### 内置方法扩展组件 / builtin 2324 | 2325 | > resty.tofu.extend.builtin 2326 | 2327 | 内置扩展api, 直接使用 tofu. 方式使用 2328 | 2329 | #### 使用配置 2330 | 2331 | ``` lua 2332 | -- 配置文件 conf/extend.lua 2333 | 2334 | extend = { 2335 | 'resty.tofu.extend.builtin' 2336 | } 2337 | ``` 2338 | 2339 | 2340 | 2341 | #### Lua API 2342 | 2343 | **tofu.response(errno, errmsg, data)** 2344 | 2345 | 响应请求,并默认以 Content-Type = application/json 方式回复 2346 | 2347 | `errno` 状态号 2348 | 2349 | `errmsg` 错误信息 2350 | 2351 | `data` 有效数据 2352 | 2353 | ``` lua 2354 | tofu.response(0, '正确', {name='tofu(豆腐)'}) 2355 | 2356 | -- 响应body 2357 | -- {"errno":0, "errmsg"="正确", "data":{"name":"tofu(豆腐)"}} 2358 | ``` 2359 | 2360 | > errno, errmsg, data 字段名称,可以在 conf/config.lua 中配置 2361 | > 2362 | > state_field 2363 | > 2364 | > message_field 2365 | > 2366 | > data_field 2367 | 2368 | 2369 | 2370 | **tofu.success(data, msg)** 2371 | 2372 | 使用 default_state 响应请求(调用 tofu.response) 2373 | 2374 | ``` lua 2375 | tofu.success() 2376 | -- 响应body 2377 | -- {"errno":0, "errmsg":""} 2378 | ``` 2379 | 2380 | 2381 | 2382 | **tofu.fail(errno, errmsg, data)** 2383 | 2384 | 响应错误请求(调用 tofu.response) 2385 | 2386 | ``` lua 2387 | tofu.fail(-1, "未知错误") 2388 | -- 响应body 2389 | -- {"errno":-1, "errmsg":"未知错误"} 2390 | ``` 2391 | 2392 | 2393 | 2394 | ### 文件监控扩展组件 / jili 2395 | 2396 | > resty.tofu.extend.jili 2397 | > 2398 | > 依赖:系统 inotify 库 2399 | 2400 | 在开发过程中我们常常在修改->重启->出错->修改->重启,不断地重复。jili(吉利)组件是个文件监控组件,当检测到项目中的文件有改动,自动按规则进行重启或重新加载 2401 | 2402 | 2403 | 2404 | #### 配置 options 2405 | 2406 | | 参数 | 类型 | 说明 | 必须 | 缺省 | 2407 | | ----- | ---- | ------------------------------------ | :--: | :--: | 2408 | | trace | bool | 是否显示控加重新加载文件时的一些信息 | 否 | true | 2409 | 2410 | 2411 | 2412 | #### 使用配置 2413 | 2414 | ``` lua 2415 | -- 配置文件 conf/extend.lua 2416 | 2417 | extend = { 2418 | { 2419 | named = 'watcher', 2420 | default = { 2421 | handle = 'resty.touf.extend.jili', 2422 | options = { 2423 | trace = true 2424 | } 2425 | } 2426 | }, 2427 | } 2428 | ``` 2429 | 2430 | 2431 | 2432 | 2433 | 2434 | 2435 | 2436 | ## 其它功能 2437 | 2438 | 2439 | 2440 | ### websocket 2441 | 2442 | 在tofu框架中使用websocket 非常简单,只需把普通的 controller "升级" 为wscontroller即可 2443 | 2444 | #### 使用 2445 | 2446 | ``` lua 2447 | local _wsc = require 'resty.tofu.wscontroller' 2448 | ``` 2449 | 2450 | 2451 | 2452 | #### Lua API 2453 | 2454 | **upgrade(handler [, opts])** 2455 | 2456 | * `handler` table, 事件接收处理器 2457 | 2458 | | 事件方法 | 说明 | 必须 | 返回值 | 2459 | | ----------------------- | ------------ | :--: | ------ | 2460 | | _open(wb) | 有新的连接 | 否 | state | 2461 | | _close(wb , state) | 连接断开 | 否 | | 2462 | | _data(wb, data , state) | 有消息进入 | 否 | | 2463 | | _ping(wb, data, state) | 收到ping消息 | 否 | | 2464 | | _pong(wb, data, state) | 收到pong消息 | 否 | | 2465 | | _timeout(wb, state) | 超时 | 否 | | 2466 | 2467 | > `wb` [lua-resty-websocket](https://github.com/openresty/lua-resty-websocket) 对象 2468 | > 2469 | > `state` 保存当前用户状态,连接关闭后会被丢弃 2470 | > 2471 | > `data` 文本消息或binary消息 2472 | 2473 | 2474 | 2475 | * `opts` table, 配置 2476 | 2477 | | 参数 | 说明 | 必须 | 缺省 | 2478 | | --------------- | ------------------ | :--: | ----- | 2479 | | timeout | 设置超时(秒) | 否 | 5000 | 2480 | | max_payload_len | 最大消息长度(byte) | 否 | 65535 | 2481 | 2482 | 2483 | 2484 | 样例, ws://xxxx/ws 或 ws://xxxx/default/ws/index 进行连接 2485 | 2486 | ``` lua 2487 | -- lua/controller/default/ws 2488 | 2489 | local _wsc = require 'resty.tofu.wscontroller' 2490 | 2491 | local _M = {} 2492 | 2493 | function _M.index() 2494 | -- webscoket 握手处理,并设置接收websocket事件的处理 2495 | _wsc.upgrade(_M) 2496 | end 2497 | 2498 | -- 2499 | -- websocket 事件处理 2500 | -- 2501 | -- 当有连接完成时 2502 | function _M._open(wb) 2503 | tofu.log.d('用户连接') 2504 | local state = {} 2505 | rturn state 2506 | end 2507 | 2508 | -- 当连接断开时 2509 | function _M._close(web, state) 2510 | tofu.log.d('断开连接') 2511 | end 2512 | 2513 | -- 当有消息时 2514 | function _M._data(wb, data, state) 2515 | tofu.log.d('接收数据:', data) 2516 | wb:send_text(data) 2517 | end 2518 | 2519 | 2520 | return _M 2521 | ``` 2522 | 2523 | 2524 | 2525 | 2526 | 2527 | 2528 | 2529 | ### 参数验证器 2530 | 2531 | tofu框架提供了一个简单的参数验证器 `resty.tofu.validation`。在tofu框架中推荐在guard阶断中使用,可以有效地验证参数,拦截请求,使得业务处理更新干净。 2532 | 2533 | #### 使用 2534 | 2535 | ``` lua 2536 | local validation = require 'tofu.validation' 2537 | ``` 2538 | 2539 | 2540 | 2541 | #### Lua API 2542 | 2543 | **options(opts)** 2544 | 2545 | 设置 2546 | 2547 | * `opts` 2548 | 2549 | | 参数 | 类型 | 说明 | 缺省 | 2550 | | ------ | --------------- | ------------------------------------------------------------ | :---: | 2551 | | mode | int | 0:检测所有参数,1:当无效参数时立即返回(中断后面的参数检测流程) | 0 | 2552 | | trim | bool | 是否去两端空白 | false | 2553 | | amend | bool | 是否自动修正参数(需指定method), 如bool参可以是 true \| 1 'true' | false | 2554 | | errmsg | string \| table | 缺省错误信息 优先级 指定>sgring>[0] | | 2555 | 2556 | errmsg 2557 | 2558 | | 参数 | 类型 | 说明 | 缺省 | 2559 | | -------- | ------ | ------------------------------------------------- | ------------ | 2560 | | [0] | string | 特殊的, 缺省信息,当没有指定错误信息时,使用该信息 | 参数错误 | 2561 | | required | string | required参数的错误信息 | 参数不参为空 | 2562 | 2563 | 2564 | 2565 | **handle(rules)** 2566 | 2567 | 参数验证 2568 | 2569 | * `rules` 2570 | 2571 | 参数校验列表 2572 | 2573 | | 校验方法名 | 说明 | 区间{min, max} | | 2574 | | ------------ | ------------ | :------------: | ---- | 2575 | | int | 整型 | 支持 | | 2576 | | float | 数值(number) | 支持 | | 2577 | | digit | 纯数字串 | 支持 | | 2578 | | string | 字符串 | 支持 | | 2579 | | date | 日期 | | | 2580 | | bool | 布尔 | | | 2581 | | alpha | 字母 | | | 2582 | | hex | 十六进制 | | | 2583 | | alphanumeric | 字母数字 | | | 2584 | 2585 | 样例 2586 | 2587 | ``` lua 2588 | -- <应用>lua/guard/default/index.lua 2589 | 2590 | local _validator = require 'resty.tofu.validation'.handle 2591 | 2592 | local _M = {} 2593 | 2594 | function _M.index() 2595 | -- 验证用户帐号与用户密码 2596 | local rules = { 2597 | -- 参数名称 = { 验证规则 } 2598 | -- 参数为必填, 字符串,长度限定在 [2, 20] 区间 2599 | account = {required=true, string={min=2, max=20}, errmsg='帐号参数错误'}, 2600 | 2601 | -- 参数为必填, 字符串,长度限定在 [6, 20] 区间 2602 | password = {required=true, string={min=6, 20}, errmsg={ string='密码长度错误' }} 2603 | } 2604 | 2605 | -- 如果参数有错误,则返回false 中断流程 2606 | local ok, errs = _validator(rules) 2607 | if not ok then 2608 | tofu.log.d(errs) 2609 | return tofu.fail(400, errs) 2610 | end 2611 | end 2612 | 2613 | return _M 2614 | ``` 2615 | 2616 | 2617 | 2618 | **register(fs, f)** 2619 | 2620 | 注册新的/自定义验证器 2621 | 2622 | * `fs` 2623 | 2624 | string 过滤器名称, table {string = function} 添加多个型式 2625 | 2626 | 保留的方法/属性 2627 | 2628 | | 名称 | 说明 | 缺省 | 2629 | | -------- | -------------------- | -------- | 2630 | | required | 是否必填参 | false | 2631 | | errmsg | 错误信息 | 参数错误 | 2632 | | value | 获取值 | | 2633 | | default | 缺省值 | | 2634 | | trim | 是否去两端空白 | false | 2635 | | amend | 是否自动修 | false | 2636 | | method | 参数使用那种方法获取 | | 2637 | 2638 | 2639 | 2640 | * `f` 2641 | 2642 | function(value, cond) -> bool 验证器 2643 | 2644 | `value` 将要校验的值 2645 | 2646 | `cond` 校验条件 2647 | 2648 | 当 value 合乎 cond 时返回 true 2649 | 2650 | 样例 2651 | 2652 | ``` lua 2653 | 2654 | -- 增一个名为 mylist 的验证器 2655 | -- 作用:值必须存在列表中 2656 | local register = require 'tofu.validation'.register 2657 | register('mylist', function(v, cond) 2658 | if 'table' == type(cond) then 2659 | for _, c in ipairs(cond) do 2660 | if v == c then return true end 2661 | end 2662 | return false 2663 | end 2664 | 2665 | return false 2666 | end) 2667 | ``` 2668 | 2669 | 这个验证器有什么用,怎么用 2670 | 2671 | ``` lua 2672 | -- <应用>/lua/guard/default/test.lua 2673 | 2674 | -- 有参数要求 2675 | -- y 年份, 必填,且只可以是 2019 2020 2021 的其中之一 2676 | -- m 月份, 可选,且只可以是 02 05 07 12 这四个月份的其中之一, 缺省为 07 2677 | 2678 | 2679 | local _validator = require 'resty.tofu.validation'.handle 2680 | local _M = {} 2681 | 2682 | function _M.date() 2683 | local rules = { 2684 | -- 参数 y 2685 | y = { required = true, mylist = {'2019', '2020', '2021'}, errmsg='年份参数错误'}, 2686 | 2687 | -- 参数 m 2688 | m = { mylist = {'02', '05', '07', '08'}, default='07', errmsg = '月份参数错误'} 2689 | } 2690 | 2691 | local ok, err = _validator(rules) 2692 | if not ok then 2693 | return tofu.fail(400, err) 2694 | end 2695 | end 2696 | 2697 | 2698 | return _M 2699 | ``` 2700 | 2701 | 可以看到我们的验证器是可以通过简单的叠加,达到处理复杂的参数验证 2702 | 2703 | 2704 | 2705 | 2706 | 2707 | ### 工具箱 2708 | 2709 | 该包的api可以独立使用,也可以使用扩展组件方式集成到tofu中 2710 | 2711 | #### 使用 2712 | 2713 | ``` lua 2714 | local _util = require 'resty.tofu.util' 2715 | ``` 2716 | 2717 | 2718 | 2719 | #### Lua API 2720 | 2721 | **split(str, delimiter)** 2722 | 2723 | 使有分隔符切分字符串,返回table(数组) 2724 | 2725 | * `str` 要处理的字符串 2726 | 2727 | * `delimiter` 分隔符号 2728 | 2729 | ``` lua 2730 | local str = '蛋散,粉肠,茂梨,碌葛' 2731 | local ret = _util.split(str, ',') 2732 | 2733 | -- 结果: ret = {'蛋散‘,’粉肠‘,’茂梨‘,’碌葛'} 2734 | ``` 2735 | 2736 | * return table 2737 | 2738 | 2739 | 2740 | **msplit(str, sep)** 2741 | 2742 | 使有分隔符切分字符串,返回table(数组)。与split不同,该方法支持多个分隔符 2743 | 2744 | * `str` 要处理的字符串 2745 | 2746 | * `sep` 分隔符号 2747 | 2748 | ``` lua 2749 | local str = '蛋散,粉肠,茂梨,碌葛|神探肥标|厨师废柴' 2750 | local ret = _util.msplit(str, ',|') 2751 | 2752 | -- 结果: ret = {'蛋散‘,’粉肠‘,’茂梨‘,’碌葛',神探肥标','厨师废柴'} 2753 | ``` 2754 | 2755 | * return string 2756 | 2757 | 2758 | 2759 | **dirname(str)** 2760 | 2761 | **getpath(str)** 2762 | 2763 | 切分路径,这两个函数作用一样 2764 | 2765 | * `str` 2766 | 2767 | ``` lua 2768 | local path = '/home/d/tofu/test.lua' 2769 | local ret = _util.getpath(str) 2770 | -- 结果: ret = '/home/d/tofu/' 2771 | ``` 2772 | 2773 | * return string 2774 | 2775 | 2776 | 2777 | **trim(str)** 2778 | 2779 | 去字符串两端空白 2780 | 2781 | * `str` 要处理的字符串 2782 | 2783 | ``` lua 2784 | local str = ' 神探肥标 厨师废柴 ' 2785 | local ret = _util.trim(str) 2786 | 2787 | --结果: ret = '神探肥标 厨师废柴' 2788 | ``` 2789 | 2790 | * return string 2791 | 2792 | 2793 | 2794 | **bin_hex(s)** 2795 | 2796 | 把目标字符串,以二进制方式转换为十六进制字符串 2797 | 2798 | * `s` 要处理的字符串 2799 | 2800 | **hex_bin(s)** 2801 | 2802 | 把十六进制字符串,以二进制方式转换为字符串 2803 | 2804 | * `s` 要处理的字符串 2805 | 2806 | ``` lua 2807 | local str = 'hi tofu!' 2808 | _util.hex_bin(str) -- 686920746f667521 2809 | _util.bin_hex('686920746f667521') -- hi tofu! 2810 | ``` 2811 | 2812 | * return string 2813 | 2814 | 2815 | 2816 | **envsubstr(str, env)** 2817 | 2818 | 字符串模板中的变量`${}`替换 2819 | 2820 | * `str` 字符串模板 2821 | 2822 | * `env` 变量表 2823 | 2824 | ``` lua 2825 | local str = 'hi ${name}!' 2826 | local ret = _util.envsubstr(str, { name='tofu' }) 2827 | 2828 | -- 结果: ret = 'hi tofu!' 2829 | ``` 2830 | 2831 | * return string 2832 | 2833 | 2834 | 2835 | **getusec()** 2836 | 2837 | 获取系统微秒级时间戳 2838 | 2839 | * return int 2840 | 2841 | 2842 | 2843 | **gettimezone()** 2844 | 2845 | 获取当前时区 2846 | 2847 | * return int 2848 | 2849 | 2850 | 2851 | **tosecond(str, tz)** 2852 | 2853 | 日期时间转换时间戳(秒) 2854 | 2855 | * `str` 支持 yyyy-mm-dd hh:ii:ss | yyyy/m/d | yyyy-m-d | hh:ii:ss 格式的日期时间字符串 2856 | 2857 | * `tz` 时区 default: 0,如北京时间东8区 2858 | 2859 | ``` lua 2860 | _util.tosecond('1970-01-01 08:00:00') -- 28800 秒 2861 | _util.tosecond('1970-01-01 08:00:00', 8) -- 这是个北京时间字符串, 返回 0 秒 2862 | ``` 2863 | 2864 | * return int 2865 | 2866 | 2867 | 2868 | **isempty(obj)** 2869 | 2870 | 是否为空 '', {}, nil, ngx.null 2871 | 2872 | * return bool 2873 | 2874 | 2875 | 2876 | **isint(v)** 2877 | 2878 | 是否是int型 2879 | 2880 | * return bool 2881 | 2882 | 2883 | 2884 | **isfloat(v)** 2885 | 2886 | 是否是 float 型 2887 | 2888 | * return bool 2889 | 2890 | 2891 | 2892 | **iscallable(f)** 2893 | 2894 | 是否可调用 2895 | 2896 | * return bool 2897 | 2898 | 2899 | 2900 | 2901 | --------------------------------------------------------------------------------