├── web ├── robots.txt ├── favicon.ico ├── css │ ├── site.css │ ├── bootstrap-theme.min.css │ └── bootstrap-theme.css └── js │ └── bootstrap.min.js ├── .gitignore ├── runtime ├── win-start.bat ├── win-stop.bat ├── stop.sh ├── start.sh ├── nginx.conf └── mime.types ├── .gitattributes ├── views ├── site │ ├── error.lua.html │ ├── index.lua.html │ ├── login.lua.html │ └── guide.lua.html └── layout │ └── main.lua.html ├── config ├── web.lua ├── memcache.lua ├── db.lua ├── lang.lua └── session.lua ├── index.lua ├── controllers └── site.lua ├── models ├── Userinfo.lua └── LoginForm.lua ├── vendor ├── ActiveRecord.lua ├── Memcache.lua ├── Request.lua ├── Application.lua ├── User.lua ├── resty │ ├── template │ │ ├── html.lua │ │ └── microbenchmark.lua │ └── template.lua ├── Mysql.lua ├── Files.lua ├── Session.lua ├── Pager.lua ├── Model.lua ├── Controller.lua ├── Util.lua └── Query.lua ├── LICENSE.txt ├── lua-releng ├── README-cn.md └── README.md /web/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.log 3 | *.pid 4 | *_temp -------------------------------------------------------------------------------- /web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylun/lua-resty-yii/HEAD/web/favicon.ico -------------------------------------------------------------------------------- /runtime/win-start.bat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylun/lua-resty-yii/HEAD/runtime/win-start.bat -------------------------------------------------------------------------------- /runtime/win-stop.bat: -------------------------------------------------------------------------------- 1 | title ֹͣlua-resty-yii 2 | 3 | taskkill /F /IM nginx.exe /T 4 | 5 | pause -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-language=Lua 2 | *.css linguist-language=Lua 3 | *.html linguist-language=Lua 4 | *.lua linguist-language=Lua 5 | -------------------------------------------------------------------------------- /runtime/stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd `dirname $0`/../ 3 | 4 | ps -ef|grep `pwd`|grep openresty |grep -v grep |awk '{print $2}'|xargs kill 5 | -------------------------------------------------------------------------------- /views/site/error.lua.html: -------------------------------------------------------------------------------- 1 | return [[ 2 | {% 3 | context.title = lang.infoNote 4 | %} 5 |
{{lang.hasError}}

{{err}}
6 | ]] -------------------------------------------------------------------------------- /config/web.lua: -------------------------------------------------------------------------------- 1 | --网站基本信息配置 2 | --Copyright (C) Yuanlun He. 3 | 4 | local webConfig = { 5 | language = 'zh-CN', 6 | staticUrl = ''--'http://your.cdn.url/',--静态资源文件路径 7 | } 8 | return webConfig 9 | -------------------------------------------------------------------------------- /config/memcache.lua: -------------------------------------------------------------------------------- 1 | --memcache 2 | --Copyright (C) Yuanlun He. 3 | 4 | local require = require 5 | 6 | return require('vendor.Memcache'):new{ 7 | host = "127.0.0.1", 8 | port = "11211", 9 | } 10 | -------------------------------------------------------------------------------- /runtime/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #切到根目录下执行命令,跟lua的require默认路径相关 3 | cd `dirname $0`/../ 4 | 5 | if [ ! -d " runtime/logs" ]; then 6 | mkdir runtime/logs 7 | fi 8 | 9 | #如果找不到openresty,请修改以下openresty的路径 10 | /usr/local/openresty/bin/openresty -p `pwd`/runtime -c `pwd`/runtime/nginx.conf 11 | -------------------------------------------------------------------------------- /config/db.lua: -------------------------------------------------------------------------------- 1 | --数据库配置 2 | --Copyright (C) Yuanlun He. 3 | 4 | local require = require 5 | 6 | return require('vendor.Mysql'):new{ 7 | host = "127.0.0.1", 8 | port = 3306, 9 | database = "mytest", 10 | user = "admin", 11 | password = "mypass", 12 | charset = "utf8", 13 | } 14 | -------------------------------------------------------------------------------- /config/lang.lua: -------------------------------------------------------------------------------- 1 | --网站语言包 2 | --Copyright (C) Yuanlun He. 3 | 4 | local zhCN = { 5 | siteName = 'Lua-Resty-Yii', 6 | infoNote = '信息提示', 7 | notFound = '没找到对应的页面', 8 | hasError = '请求错误,请联系管理员', 9 | loadFail = '加载数据错误', 10 | required = '请填写此字段', 11 | matchErr = '填写格式错误', 12 | emailErr = '请填写邮箱地址', 13 | } 14 | return zhCN 15 | -------------------------------------------------------------------------------- /config/session.lua: -------------------------------------------------------------------------------- 1 | --数据库配置 2 | --Copyright (C) Yuanlun He. 3 | 4 | local require = require 5 | local ngx_shared = ngx.shared 6 | 7 | local _M = require('vendor.Session') 8 | --_M._VERSION= '0.01' 9 | --_M.exptime = 1800, --修改session失效时间时,打开默认30分钟 10 | --_M.cache = ngx_shared['session_cache'], --使用ngx的缓存时打开,此为默认项 11 | --使用memcached缓存打开以下配置 12 | --_M.cache = require("vendor.Memcache"):new{host="127.0.0.1",port="11211"}, 13 | return _M 14 | -------------------------------------------------------------------------------- /index.lua: -------------------------------------------------------------------------------- 1 | --web  入口 2 | --version:1.0.0 3 | --Copyright (C) Yuanlun He. 4 | local require = require 5 | local package = package 6 | local pack_path = package.path 7 | local find = string.find 8 | local sub = string.sub 9 | local ngx = ngx 10 | local ngx_var = ngx.var 11 | local ngx_header = ngx.header 12 | 13 | --设置默认输出类型 14 | ngx_header["Content-Type"] = "text/html; charset=utf-8" 15 | 16 | --根目录 17 | local root = ngx_var.document_root:sub(1,-4) 18 | 19 | --添加类加载路劲 20 | local p = root.."?.lua;"..root.."views/?.lua.html;" 21 | if pack_path:sub(1,#p)~=p then package.path = p..pack_path end 22 | 23 | --初始化请求生命周期内的全局变量 24 | ngx.ctx = require("vendor.Application"):new() 25 | 26 | --设置根目录 27 | ngx.ctx.web.root = root 28 | 29 | --执行请求 30 | ngx.ctx:out(ngx.ctx:run()) -------------------------------------------------------------------------------- /controllers/site.lua: -------------------------------------------------------------------------------- 1 | --site Controller 2 | --version:0.0.1 3 | --Copyright (C) Yuanlun He. 4 | 5 | local Pager = require("vendor.Pager") 6 | 7 | local _M = require("vendor.Controller"):new{_VERSION='0.01'} 8 | 9 | function _M:indexAction() 10 | return self:render('index') 11 | end 12 | 13 | function _M:loginAction() 14 | if self.user.isLogin then return self:goHome() end 15 | 16 | local model = require('models.LoginForm'):new() 17 | 18 | if model:load(self.request.post()) and model:login(self.user) then 19 | return self:goHome() 20 | end 21 | 22 | return self:render('login',{ 23 | model = model 24 | }) 25 | end 26 | 27 | function _M:logoutAction() 28 | self.user.logout() 29 | return self:goHome() 30 | end 31 | 32 | function _M:guideAction() 33 | return self:render('guide') 34 | end 35 | 36 | return _M 37 | -------------------------------------------------------------------------------- /views/site/index.lua.html: -------------------------------------------------------------------------------- 1 | return [[ 2 | {% 3 | context.title = '首页' 4 | %} 5 |
6 | 7 |
8 |

恭喜!

9 |

您已经成功创建了一个Lua-Resty-Yii应用。

10 |
11 | 12 |
13 | 14 |
15 |
16 |

快速

17 |

Lua-Resty-Yii 是在OpenResty的基础上开发的Web框架,具有强大的运行性能。

18 |
19 |
20 |

简单

21 |

Lua-Resty-Yii 是参考PHP的著名开源框架Yii2搭建,使用十分简单易用,易于开发扩展。

22 |
23 |
24 |

专业

25 |

Lua-Resty-Yii 可帮助您开发清洁和可重用的代码。它遵循了 MVC 模式,确保了清晰分离逻辑层和表示层。

26 |
27 |
28 | 29 |
30 |
31 | ]] -------------------------------------------------------------------------------- /models/Userinfo.lua: -------------------------------------------------------------------------------- 1 | --用户模块 2 | --Copyright (C) Yuanlun He. 3 | 4 | --[[如果是数据库表格,可以如下: 5 | return require('ActiveRecord'){ 6 | 7 | tableName = function() 8 | return 'userinfos' 9 | end 10 | } 11 | ]] 12 | 13 | local require = require 14 | local tonumber = tonumber 15 | 16 | local util = require("vendor.Util") 17 | 18 | local _M = require("vendor.Model"):new{_version='0.0.1'} 19 | 20 | local userinfos = { 21 | { 22 | id = '100', 23 | username = 'admin', 24 | password = 'admin', 25 | }, 26 | { 27 | id = '101', 28 | username = 'demo', 29 | password = 'demo', 30 | } 31 | } 32 | 33 | function _M.getUserByName(username) 34 | local user = nil 35 | for k,v in pairs(userinfos) do 36 | if v.username==username then 37 | user = v 38 | end 39 | end 40 | return user 41 | end 42 | 43 | return _M 44 | -------------------------------------------------------------------------------- /runtime/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; #测试期间建议设置为1,能测试出是否有缓存或者变量相互污染等问题 2 | error_log logs/error.log; 3 | events { 4 | worker_connections 1024; 5 | } 6 | http { 7 | 8 | #设定mime类型,类型由mime.type文件定义 9 | include mime.types; 10 | default_type text/html; 11 | 12 | lua_shared_dict session_cache 10m; #存储session的缓存,在线服,内存请设置大一些 13 | 14 | resolver 8.8.8.8; 15 | 16 | server { 17 | listen 8080; 18 | 19 | server_name localhost; 20 | charset utf-8; 21 | root ../web; 22 | 23 | client_max_body_size 512m; 24 | 25 | #正式模式 26 | location = / { 27 | content_by_lua_file ../index.lua; 28 | } 29 | 30 | #测试模式,访问/test/* 31 | location ~ '^/test/(\w+(?:\/\w+)*)$' { 32 | lua_code_cache off; 33 | content_by_lua_file ../$1.lua; 34 | } 35 | 36 | location ~ ^/(images|javascript|js|css|flash|media|static)/ { 37 | expires 30d;#过期30天 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /views/site/login.lua.html: -------------------------------------------------------------------------------- 1 | return [[ 2 | {% 3 | context.title = '请登录' 4 | %} 5 |
6 |

账号登录

7 |
8 |
9 | 10 |
 {{model.err.username}}
11 |
12 |
13 | 14 |
 {{model.err.password}}
15 |
16 |
17 | 18 | 19 |
20 |
21 |
22 | ]] -------------------------------------------------------------------------------- /vendor/ActiveRecord.lua: -------------------------------------------------------------------------------- 1 | --活动记录 2 | --version:'0.0.1' 3 | --Copyright (C) Yuanlun He. 4 | 5 | local require = require 6 | local type = type 7 | 8 | local Model = require('vendor.Model') 9 | local Query = require('vendor.Query') 10 | 11 | return function(_M) 12 | 13 | _M = Model:new(_M) --继承Model 14 | _M._VERSION = '0.01' 15 | _M.db = _M.db or require('config.db') 16 | 17 | --返回查询对象 18 | function _M.new(data) 19 | local query = Query(data) 20 | query.use(_M.db).from(_M.tableName()) 21 | return query 22 | end 23 | 24 | --返回查询对象 25 | function _M.find(s) 26 | local query = _M.new() 27 | if type(s)=='table' then query.where(s) end 28 | if type(s)=='string' then query.select(s) end 29 | return query 30 | end 31 | 32 | --返回一行符合条件的结果集 33 | function _M.findOne(cond) 34 | return _M.new().where(cond).one() 35 | end 36 | 37 | --返回所有符合条件的结果集 38 | function _M.findAll(cond) 39 | return _M.new().where(cond).all() 40 | end 41 | 42 | --根据sql返回结果集 43 | function _M.findSql(sql) 44 | return _M.new().sql(sql) 45 | end 46 | 47 | return _M 48 | 49 | end 50 | -------------------------------------------------------------------------------- /vendor/Memcache.lua: -------------------------------------------------------------------------------- 1 | --memchache处理类 2 | --version:0.0.1 3 | --Copyright (C) Yuanlun He. 4 | 5 | local setmetatable = setmetatable 6 | local require = require 7 | 8 | local memcached = require "resty.memcached" 9 | 10 | local _M = { 11 | _VERSION='0.01', 12 | timeout=1000, 13 | host = "127.0.0.1", 14 | port = "11211", 15 | } 16 | 17 | function _M.__index(self, key) 18 | return _M[key] or function(...) 19 | 20 | local memc, err = memcached:new() 21 | if not memc then 22 | return nil,err 23 | end 24 | 25 | memc:set_timeout(self.timeout) -- 1 sec 26 | 27 | local ok, err = memc:connect(self.host, self.port) 28 | if not ok then 29 | return nil,err 30 | end 31 | 32 | local ok, err = memc[key](memc,...) 33 | 34 | memc:set_keepalive(10000, 100) --放回连接池 35 | 36 | return ok, err 37 | end 38 | end 39 | 40 | function _M.new(self,config) 41 | config = config or {} 42 | return setmetatable(config,self) 43 | end 44 | 45 | return _M 46 | -------------------------------------------------------------------------------- /models/LoginForm.lua: -------------------------------------------------------------------------------- 1 | --登录逻辑处理 2 | --version:0.0.1 3 | --Copyright (C) Yuanlun He. 4 | 5 | local require = require 6 | 7 | local _M = require("vendor.Model"):new{_version='0.0.1'} 8 | 9 | local rememberMe = false 10 | local Userinfo = require('models.Userinfo') 11 | 12 | function _M.rules() 13 | return { 14 | {{'username','password'}, 'trim'}, 15 | {'username', 'required',message='请填写登录账号'}, 16 | --{'username', 'email'}, --用户名必须为email时设置 17 | {'password', 'required',message='请填写登录密码'}, 18 | --使用自定义方法校验参数 19 | {'password','checkPass'} 20 | } 21 | end 22 | 23 | function _M:checkPass(key) 24 | if self:hasErrors() then return end 25 | 26 | local user = Userinfo.getUserByName(self.username) 27 | if not user then 28 | self:addError('username','账号不存在') 29 | elseif user.password ~= self.password then 30 | self:addError('password','密码错误') 31 | else 32 | self.userinfo = user 33 | end 34 | end 35 | 36 | function _M:login(user) 37 | if not self:validate() then return false end 38 | 39 | return user.login(self.userinfo, rememberMe and 3600*24*30 or 0) 40 | end 41 | 42 | return _M 43 | -------------------------------------------------------------------------------- /vendor/Request.lua: -------------------------------------------------------------------------------- 1 | --请求处理类 2 | --version:0.0.1 3 | --Copyright (C) Yuanlun He. 4 | 5 | local setmetatable = setmetatable 6 | local find = string.find 7 | local pairs = pairs 8 | local type = type 9 | local ngx_req = ngx.req 10 | local ngx_var = ngx.var 11 | 12 | local _M = {_VERSION='0.01'} 13 | 14 | function _M.get(key,default) 15 | local args = ngx_req.get_uri_args() 16 | if key and args then 17 | return args[key] or default 18 | else 19 | return args 20 | end 21 | end 22 | 23 | function _M.cookie(key,default) 24 | if key and ngx_var then 25 | return ngx_var["cookie_" .. key] or default 26 | end 27 | return ngx_var.http_cookie 28 | end 29 | 30 | function _M.post(key,default) 31 | ngx_req.read_body() 32 | local args = ngx_req.get_post_args() 33 | if key and args then 34 | return args[key] or default 35 | else 36 | return args 37 | end 38 | end 39 | 40 | function _M.isPostFile() 41 | local header = ngx_var.content_type 42 | if not header then return false end 43 | 44 | if type(header) == "table" then 45 | header = header[1] 46 | end 47 | return find(header, "multipart") and true or false 48 | end 49 | 50 | return _M 51 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 He Yuanlun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | http://www.opensource.org/licenses/mit-license.php -------------------------------------------------------------------------------- /vendor/Application.lua: -------------------------------------------------------------------------------- 1 | --上下文,单例模式 2 | --version:0.0.1 3 | --Copyright (C) Yuanlun He. 4 | 5 | local require = require 6 | local pairs = pairs 7 | local pcall = pcall 8 | local sub = string.sub 9 | local ngx = ngx 10 | 11 | local _M = { 12 | _VERSION='0.01', 13 | web = require("config.web"), --站点配置信息表 14 | lang = require("config.lang"), --语言包配置信息 15 | session = require("config.session"),--站点Session类 16 | user = require("vendor.User"), --站点用户工具类 17 | request = require("vendor.Request"),--站点请求处理类 18 | } 19 | 20 | function _M:out(res) 21 | if ngx.status>0 then ngx.exit(ngx.status) end 22 | if res then ngx.say(res) end 23 | end 24 | 25 | local function getController(app) 26 | local con = require('vendor.Controller') 27 | local act = app.request.get('act') or con.class 28 | local p = act:find("%.") 29 | local class = p and act:sub(1,p-1) or act 30 | 31 | local ok,res = pcall(require,"controllers."..class) 32 | if not ok then ngx.log(ngx.ERR,res) end 33 | res = (ok and res or con):new(app) 34 | res.class = class 35 | res.action = ok and (p and act:sub(p+1) or res.action) or 'notFound' 36 | 37 | return res 38 | end 39 | 40 | function _M:new() 41 | local app = {} 42 | for k,v in pairs(self) do 43 | app[k] = v 44 | end 45 | 46 | return getController(app) 47 | end 48 | 49 | return _M 50 | -------------------------------------------------------------------------------- /vendor/User.lua: -------------------------------------------------------------------------------- 1 | --用户管理类 2 | --version:0.0.1 3 | --Copyright (C) Yuanlun He. 4 | 5 | local setmetatable = setmetatable 6 | local type = type 7 | 8 | local util = require "vendor.Util" 9 | local request = require "vendor.Request" 10 | local session = require "vendor.Session" 11 | 12 | local _M = {_VERSION='0.01'} 13 | 14 | function _M.login(user,exprise) 15 | if type(user)~='table' then return false end 16 | local res = nil 17 | exprise = exprise or user.exprise 18 | if exprise and exprise > 0 then 19 | user.exprise = exprise 20 | sso = util.aes_encrypt(util.json_encode(user)) 21 | res = util.set_cookie('_sso',sso,exprise) 22 | else 23 | --session.user = user或session['user'] = user 24 | res = session.set('user',user) 25 | end 26 | return res~=nil 27 | end 28 | 29 | function _M.logout() 30 | util.set_cookie('_sso',nil) 31 | session['user'] = nil 32 | end 33 | 34 | function _M.__index(table, key) 35 | local userinfo = nil 36 | local sso = request.cookie('_sso') 37 | if sso then 38 | userinfo = util.json_decode(util.aes_decrypt(sso)) 39 | else 40 | userinfo = session['user'] 41 | end 42 | 43 | if key == 'isLogin' then return userinfo end 44 | 45 | return userinfo and userinfo[key] or nil 46 | end 47 | 48 | function _M.__newindex(table, key, value) 49 | --设置此类为只读对象,不可以设置新属性 50 | end 51 | 52 | return setmetatable(_M, _M) 53 | -------------------------------------------------------------------------------- /vendor/resty/template/html.lua: -------------------------------------------------------------------------------- 1 | local template = require "resty.template" 2 | local setmetatable = setmetatable 3 | local escape = template.escape 4 | local concat = table.concat 5 | local pairs = pairs 6 | local type = type 7 | 8 | local function tag(name, content, attr) 9 | local r, a, content = {}, {}, content or attr 10 | r[#r + 1] = "<" 11 | r[#r + 1] = name 12 | if attr then 13 | for k, v in pairs(attr) do 14 | if type(k) == "number" then 15 | a[#a + 1] = escape(v) 16 | else 17 | a[#a + 1] = k .. '="' .. escape(v) .. '"' 18 | end 19 | end 20 | if #a > 0 then 21 | r[#r + 1] = " " 22 | r[#r + 1] = concat(a, " ") 23 | end 24 | end 25 | if type(content) == "string" then 26 | r[#r + 1] = ">" 27 | r[#r + 1] = escape(content) 28 | r[#r + 1] = "" 31 | else 32 | r[#r + 1] = " />" 33 | end 34 | return concat(r) 35 | end 36 | 37 | local html = { __index = function(_, name) 38 | return function(attr) 39 | if type(attr) == "table" then 40 | return function(content) 41 | return tag(name, content, attr) 42 | end 43 | else 44 | return tag(name, attr) 45 | end 46 | end 47 | end } 48 | 49 | template.html = setmetatable(html, html) 50 | 51 | return template.html 52 | -------------------------------------------------------------------------------- /vendor/Mysql.lua: -------------------------------------------------------------------------------- 1 | --memchache处理类 2 | --version:0.0.1 3 | --Copyright (C) Yuanlun He. 4 | 5 | local require = require 6 | 7 | local mysql = require "resty.mysql" 8 | 9 | local _M = { 10 | _VERSION='0.01', 11 | host = "127.0.0.1", 12 | port = 3306, 13 | database = "mytest", 14 | user = "root", 15 | password = "", 16 | charset = "utf8", 17 | timeout = 1000, 18 | max_packet_size = 1024 * 1024, 19 | } 20 | 21 | _M.__index = _M 22 | 23 | function _M.new(self,config) 24 | config = config or {} 25 | return setmetatable(config,self) 26 | end 27 | 28 | function _M.query(self,... ) 29 | local db, err = mysql:new() 30 | if not db then return nil,err end 31 | 32 | db:set_timeout(self.timeout) -- 1 sec 33 | 34 | local ok, err, errcode, sqlstate = db:connect{ 35 | host = self.host, 36 | port = self.port, 37 | database = self.database, 38 | user = self.user, 39 | password = self.password, 40 | max_packet_size = self.max_packet_size, 41 | } 42 | 43 | if not ok then return nil,err end 44 | 45 | local ok, err = db:get_reused_times() 46 | if (not ok or ok==0) and self.charset then 47 | db:query('SET NAMES '..self.charset) 48 | end 49 | 50 | local res, err, errcode, sqlstate = db:query(...) 51 | if not ok then return nil,err end 52 | 53 | -- 放入连接池 54 | db:set_keepalive(10000, 100) 55 | 56 | return res, err 57 | end 58 | 59 | return _M 60 | 61 | -------------------------------------------------------------------------------- /vendor/Files.lua: -------------------------------------------------------------------------------- 1 | --文件上传处理类 2 | --version:0.0.1 3 | --Copyright (C) Yuanlun He. 4 | 5 | local setmetatable = setmetatable 6 | local len = string.len 7 | local type = type 8 | local match = ngx.re.match 9 | local io_open = io.open 10 | 11 | local upload = require "resty.upload" 12 | 13 | local _M = {_VERSION='0.01'} 14 | 15 | -- 接收上传文件,保存为savename 16 | function _M.receive(key,savename) 17 | local chunk_size = 4096 18 | local form,err = upload:new(chunk_size) 19 | if not form then 20 | return nil,err 21 | end 22 | form:set_timeout(10000) -- 10 sec 23 | 24 | local file,filename,filelen = nil,nil,0 25 | while true do 26 | local typ, res, err = form:read() 27 | if not typ then 28 | return nil,err 29 | end 30 | if typ == "header" and res[1] ~= "Content-Type" then 31 | local ma = match(res[2],'(.+)name="(.+)"(.+)filename="(.+)"(.*)') 32 | if savename and ma and ma[2]==key then 33 | file = io_open(savename,"w+") 34 | if not file then 35 | return nil,'failed to open file '..savename 36 | end 37 | filename = ma[4] 38 | end 39 | elseif typ == "body" and file then 40 | filelen = filelen + len(res) 41 | file:write(res) 42 | elseif typ == "part_end" and file then 43 | file:close() 44 | file = nil 45 | elseif typ == "eof" then 46 | break 47 | end 48 | end 49 | if filename then 50 | return {name=savename,len=filelen,filename=filename},nil 51 | else 52 | return nil,'not found upload file for '..key 53 | end 54 | end 55 | 56 | return _M 57 | 58 | -------------------------------------------------------------------------------- /views/layout/main.lua.html: -------------------------------------------------------------------------------- 1 | return [[ 2 | 3 | 4 | 5 | 6 | 7 | {{title and lang.siteName..'-'..title or lang.siteName}} 8 | 9 | 10 | 11 | 12 |
13 | 31 |
{*contentView*}
32 |
33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | ]] -------------------------------------------------------------------------------- /vendor/Session.lua: -------------------------------------------------------------------------------- 1 | --session处理类 2 | --version:0.0.1 3 | --Copyright (C) Yuanlun He. 4 | 5 | local setmetatable = setmetatable 6 | local require = require 7 | local next = next 8 | local ipairs = ipairs 9 | local sub = string.sub 10 | local find = string.find 11 | local ngx_md5 = ngx.md5 12 | local ngx_now = ngx.now 13 | local ngx_var = ngx.var 14 | local ngx_header = ngx.header 15 | local ngx_shared = ngx.shared 16 | 17 | local util = require("vendor.Util") 18 | local req = require("vendor.Request") 19 | 20 | local _M = { 21 | _VERSION='0.01', 22 | exptime = 1800, --默认session失效时间30分钟 23 | cache = ngx_shared['session_cache'], --默认使用ngx的缓存 24 | } 25 | 26 | local function get_sessionid() --获取已设置的cookie 27 | local cookies = ngx_header['Set-Cookie'] or {} 28 | cookies = type(cookies)=='table' and cookies or {cookies} 29 | for _,v in ipairs(cookies) do 30 | local i,j = v:find('=') 31 | if i and '_sessionid'==v:sub(1,i-1) then 32 | return v:sub(i+1) 33 | end 34 | end 35 | return nil 36 | end 37 | 38 | function _M.start() 39 | local id = req.cookie('_sessionid') or get_sessionid() 40 | if not id then 41 | local ip = ngx_var.remote_addr or '' 42 | id = ngx_md5(ip..ngx_now()) 43 | util.set_cookie('_sessionid',id) 44 | end 45 | return 'session_'..id 46 | end 47 | 48 | function _M.get(key,default) 49 | local id = _M.start() 50 | --ngx.say('get',id) 51 | local vars = util.json_decode(_M.cache:get(id)) or {} 52 | return not key and vars or vars[key] or default 53 | --]] return _M.cache:get(id..'_'..key) 54 | end 55 | 56 | function _M.set(key, value) 57 | local id = _M.start() 58 | --ngx.say('set',id,key) 59 | local vars = _M.get() 60 | vars[key] = value 61 | return _M.cache:set(id, util.json_encode(vars), _M.exptime) 62 | --]] return _M.cache:set(id..'_'..key, value, _M.exptime) 63 | end 64 | 65 | --直接通过值来获取对应的session值 66 | _M.__index = function(self, key) 67 | return _M.get(key) 68 | end 69 | 70 | --直接通过值来设置session 71 | _M.__newindex = function(self, key, value) 72 | return _M.set(key, value) 73 | end 74 | 75 | return setmetatable(_M, _M) 76 | 77 | --]] return _M 78 | 79 | -------------------------------------------------------------------------------- /vendor/Pager.lua: -------------------------------------------------------------------------------- 1 | --session处理类 2 | --version:0.0.1 3 | --Copyright (C) Yuanlun He. 4 | 5 | local setmetatable = setmetatable 6 | local require = require 7 | local pairs = pairs 8 | local type = type 9 | local tonumber = tonumber 10 | local ceil = math.ceil 11 | local floor = math.floor 12 | local ngx_get_args = ngx.req.get_uri_args 13 | 14 | return function (_M) 15 | _M = _M or {} 16 | _M._VERSION = '0.01' 17 | _M.pageSize = _M.pageSize or 10 --每页显示条数 18 | _M.buttonSize = _M.buttonSize or 10 --显示的按钮数 19 | _M.totalCount = _M.totalCount or 0 --总页数 20 | _M.pageParam = _M.pageParam or 'page' --分页的参数 21 | _M.class = _M.class or 'pagination' --分页的样式 22 | _M.prevButton = _M.prevButton or '«' --向前翻按钮 23 | _M.nextButton = _M.nextButton or '»' --向后翻按钮 24 | 25 | local get = ngx_get_args() 26 | _M.now = floor(tonumber(get[_M.pageParam]) or 0) 27 | _M.now = (_M.now and _M.now>1) and _M.now or 1 28 | _M.offset = (_M.now-1) * _M.pageSize 29 | if _M.offset>=_M.totalCount then 30 | _M.now = 1 31 | _M.offset = 0 32 | end 33 | _M.limit = _M.pageSize 34 | 35 | _M.render = function(opts) 36 | if _M.now==1 and _M.totalCount <= (_M.offset+_M.pageSize) then return nil end 37 | 38 | opts = opts or {} 39 | for k,v in pairs(opts) do _M[k] = v end 40 | 41 | local max = ceil(_M.totalCount/_M.pageSize) 42 | 43 | local url = '?' 44 | get[_M.pageParam] = nil 45 | for k,v in pairs(get) do 46 | url = url..k..'='..v..'&' 47 | end 48 | url =url .._M.pageParam..'=' 49 | 50 | local str = '' 73 | end 74 | 75 | return _M 76 | 77 | end 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /web/css/site.css: -------------------------------------------------------------------------------- 1 | body,button, input, select, textarea,h1 ,h2, h3, h4, h5, h6 { 2 | font-family: Microsoft YaHei,Tahoma, Helvetica, Arial, "\5b8b\4f53", sans-serif; 3 | } 4 | 5 | html, 6 | body { 7 | height: 100%; 8 | } 9 | 10 | .wrap { 11 | min-height: 100%; 12 | height: auto; 13 | margin: 0 auto -60px; 14 | padding: 0 0 60px; 15 | } 16 | 17 | .wrap > .container { 18 | padding: 70px 15px 20px; 19 | } 20 | 21 | .footer { 22 | height: 60px; 23 | background-color: #f5f5f5; 24 | border-top: 1px solid #ddd; 25 | padding-top: 20px; 26 | } 27 | 28 | .jumbotron { 29 | text-align: center; 30 | background-color: transparent; 31 | } 32 | 33 | .jumbotron .btn { 34 | font-size: 21px; 35 | padding: 14px 24px; 36 | } 37 | 38 | .not-set { 39 | color: #c55; 40 | font-style: italic; 41 | } 42 | 43 | /* add sorting icons to gridview sort links */ 44 | a.asc:after, a.desc:after { 45 | position: relative; 46 | top: 1px; 47 | display: inline-block; 48 | font-family: 'Glyphicons Halflings'; 49 | font-style: normal; 50 | font-weight: normal; 51 | line-height: 1; 52 | padding-left: 5px; 53 | } 54 | 55 | a.asc:after { 56 | content: /*"\e113"*/ "\e151"; 57 | } 58 | 59 | a.desc:after { 60 | content: /*"\e114"*/ "\e152"; 61 | } 62 | 63 | .sort-numerical a.asc:after { 64 | content: "\e153"; 65 | } 66 | 67 | .sort-numerical a.desc:after { 68 | content: "\e154"; 69 | } 70 | 71 | .sort-ordinal a.asc:after { 72 | content: "\e155"; 73 | } 74 | 75 | .sort-ordinal a.desc:after { 76 | content: "\e156"; 77 | } 78 | 79 | .grid-view th { 80 | white-space: nowrap; 81 | } 82 | 83 | .hint-block { 84 | display: block; 85 | margin-top: 5px; 86 | color: #999; 87 | } 88 | 89 | .error-summary { 90 | color: #a94442; 91 | background: #fdf7f7; 92 | border-left: 3px solid #eed3d7; 93 | padding: 10px 20px; 94 | margin: 0 0 15px 0; 95 | } 96 | 97 | .header { 98 | height: 50px; 99 | line-height: 50px; 100 | background-color: #333; 101 | font-size:18px; 102 | } 103 | 104 | .borderdiv{ 105 | border: 1px solid #d0d0d0; 106 | border-radius: 5px; 107 | box-shadow: 1px 1px 3px #d0d0d0; 108 | z-index: 12; 109 | text-align: center; 110 | } 111 | .borderdiv input{ 112 | margin:0 0 10px 0; 113 | width: 240px; 114 | display:inline; 115 | } 116 | .indexpage{ 117 | margin:0 auto; 118 | float:right; 119 | } 120 | .text-hidden{ 121 | overflow: hidden; 122 | text-overflow:ellipsis; 123 | white-space:nowrap; 124 | } 125 | .radio{ 126 | display: inline-block; 127 | } 128 | 129 | a.active{ 130 | color: red; 131 | /*font-weight: bold;*/ 132 | } 133 | -------------------------------------------------------------------------------- /vendor/Model.lua: -------------------------------------------------------------------------------- 1 | --model基类 2 | --version:'0.0.1' 3 | --Copyright (C) Yuanlun He. 4 | 5 | local setmetatable = setmetatable 6 | local require = require 7 | local ipairs = ipairs 8 | local pairs = pairs 9 | local next = next 10 | local type = type 11 | local match = ngx.re.match 12 | 13 | local lang = require("config.lang") 14 | local util = require("vendor.Util") 15 | 16 | local _M = {_VERSION='0.01'} 17 | 18 | --新建Model 19 | function _M:new(o) 20 | o = o or {} 21 | o.err = {} --存储错误信息 22 | for k,v in pairs(self) do 23 | if not o[k] then 24 | o[k] = v 25 | end 26 | end 27 | return o 28 | end 29 | 30 | --加载数据 31 | function _M:load(data) 32 | if not next(data) then return false end 33 | for k,v in pairs(data) do 34 | if type(v)~='function' then 35 | self[k] = v 36 | end 37 | end 38 | return true 39 | end 40 | 41 | --校验规则 42 | function _M:rules() 43 | return {} 44 | end 45 | 46 | --是否有错 47 | function _M:hasErrors() 48 | return self.err and next(self.err) or false 49 | end 50 | 51 | --添加错误 52 | function _M:addError(key,err) 53 | self.err = self.err or {} 54 | self.err[key] = err 55 | end 56 | 57 | --根据规则校验参数 58 | function _M:validate() 59 | local rules = self:rules() 60 | if not next(rules) then return true end 61 | 62 | for k,v in pairs(rules) do 63 | local func = v[2] 64 | self[func](self,v[1],v) 65 | end 66 | return not self:hasErrors() 67 | end 68 | 69 | --参数trim 70 | function _M:trim(key) 71 | key = type(key)=='table' and key or {key} 72 | for _,k in ipairs(key) do 73 | if self[k] then self[k] = util.trim(self[k]) end 74 | end 75 | end 76 | 77 | --参数intval 78 | function _M:intval(key) 79 | key = type(key)=='table' and key or {key} 80 | for _,k in ipairs(key) do 81 | if self[k] then self[k] = util.intval(self[k]) end 82 | end 83 | end 84 | 85 | --检查参数是否为空 86 | function _M:required(key,rule) 87 | key = type(key)=='table' and key or {key} 88 | for _,k in ipairs(key) do 89 | if self.err[k]==nil and (self[k]==nil or self[k]=='') then 90 | local msg = rule.message or lang.required 91 | self:addError(k,msg) 92 | end 93 | end 94 | end 95 | 96 | --检查参数是否符合正则规则 97 | function _M:match(key,rule) 98 | if not rule.pattern then return end 99 | key = type(key)=='table' and key or {key} 100 | for _,k in ipairs(key) do 101 | -- 用ngx的正则性能更高,参数"o"是开启缓存必须的 102 | if self.err[k]==nil and match(self[k], rule.pattern, "o")==nil then 103 | local msg = rule.message or lang.matchErr 104 | self:addError(k,msg) 105 | end 106 | end 107 | end 108 | 109 | --检查参数是否是邮箱地址 110 | function _M:email(key,rule) 111 | key = type(key)=='table' and key or {key} 112 | rule.pattern = [[^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$]] 113 | for _,k in ipairs(key) do 114 | if self.err[k]==nil and match(self[k], rule.pattern, "o")==nil then 115 | local msg = rule.message or lang.emailErr 116 | self:addError(k,msg) 117 | end 118 | end 119 | end 120 | 121 | return _M 122 | -------------------------------------------------------------------------------- /vendor/Controller.lua: -------------------------------------------------------------------------------- 1 | --controller基类 2 | --version:'0.0.1' 3 | --Copyright (C) Yuanlun He. 4 | 5 | local setmetatable = setmetatable 6 | local require = require 7 | local table = table 8 | local pcall = pcall 9 | local pairs = pairs 10 | local type = type 11 | local find = string.find 12 | local sub = string.sub 13 | local ngx = ngx 14 | local ngx_log = ngx.log 15 | local ngx_var = ngx.var 16 | local ngx_header = ngx.header 17 | 18 | local _M = { 19 | _VERSION='0.01', 20 | class='site',--默认访问的class为site 21 | action='index',--默认访问的action为index 22 | layout='main',--默认使用的布局为main 23 | } 24 | 25 | --新建controller实例 26 | function _M:new(o) 27 | o = o or {} 28 | for k,v in pairs(self) do 29 | if not o[k] then 30 | o[k] = v 31 | end 32 | end 33 | return o 34 | end 35 | 36 | --执行controller的Action方法 37 | function _M:run() 38 | local mod = self[self.action..'Action'] 39 | if type(mod)~='function' then 40 | return self:notFound() 41 | end 42 | 43 | if not self:before() then return end 44 | local ok,res = pcall(mod,self) 45 | if not ok then 46 | ngx_log(ngx.ERR,res) 47 | local err = res:sub(1,res:find("\n")) 48 | res = self:showError(err) 49 | end 50 | self:after(ok,res) 51 | 52 | return res 53 | end 54 | 55 | --执行controller的Action前的过滤方法 56 | function _M:before() 57 | return true 58 | end 59 | 60 | --执行controller的Action后的追加方法 61 | function _M:after() 62 | return true 63 | end 64 | 65 | --返回首页 66 | function _M:goHome() 67 | return self:redirect(ngx_var.uri) 68 | end 69 | 70 | --返回上一页 71 | function _M:goBack() 72 | local url = ngx_var.http_referer 73 | return self:redirect(url) 74 | end 75 | 76 | --渲染页面 77 | function _M:render(view,params) 78 | params = params or {} 79 | setmetatable(params, {__index = self}) 80 | 81 | local template = require("vendor.resty.template") 82 | 83 | local dir = view:find('/') and view or self.class..'/'..view 84 | 85 | params.contentView = template.compile(require(dir))(params) 86 | 87 | return template.compile(require("layout/"..self.layout))(params) 88 | end 89 | 90 | --页面没有找到 91 | function _M:notFound() 92 | return self:showError(self.lang.notFound) 93 | end 94 | 95 | --显示错误页面 96 | function _M:showError(err) 97 | return self:render('site/error',{err=err}) 98 | end 99 | 100 | --页面跳转 101 | function _M:redirect(url) 102 | url = url or (type(self)=='string' and self or ngx_var.uri) 103 | ngx_header["Location"] = url 104 | ngx.status = 302 105 | return nil 106 | end 107 | 108 | --根据参数生成链接地址 109 | function _M:createUrl(params) 110 | params = params or {} 111 | local url = ngx_var.uri..'?' 112 | local get = self.request.get() 113 | for k,v in pairs(get) do 114 | if params[k]==nil then 115 | url = url..k..'='..v..'&' 116 | end 117 | end 118 | if type(params)=='table' then 119 | for k,v in pairs(params) do 120 | url = url..k..'='..v..'&' 121 | end 122 | else 123 | url = url..params 124 | end 125 | return url:sub(1,-1) 126 | end 127 | 128 | return _M 129 | -------------------------------------------------------------------------------- /lua-releng: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Getopt::Std; 7 | 8 | my (@luas, @tests); 9 | 10 | my %opts; 11 | getopts('Lse', \%opts) or die "Usage: lua-releng [-L] [-s] [-e] [files]\n"; 12 | 13 | my $silent = $opts{s}; 14 | my $stop_on_error = $opts{e}; 15 | my $no_long_line_check = $opts{L}; 16 | 17 | my $check_lua_ver = "luac -v | awk '{print\$2}'| grep 5.2"; 18 | my $output = `$check_lua_ver`; 19 | if ($output eq '') { 20 | die "ERROR: lua-releng ONLY supports Lua 5.1!\n"; 21 | } 22 | 23 | if ($#ARGV != -1) { 24 | @luas = @ARGV; 25 | 26 | } else { 27 | @luas = map glob, qw{ *.lua config/*.lua models/*.lua controllers/*.lua vendor/*.lua vendor/*/*.lua vendor/*/*/*.lua}; 28 | if (-d 't') { 29 | @tests = map glob, qw{ t/*.t t/*/*.t t/*/*/*.t }; 30 | } 31 | } 32 | 33 | for my $f (sort @luas) { 34 | process_file($f); 35 | } 36 | 37 | for my $t (@tests) { 38 | blank(qq{grep -H -n --color -E '\\--- ?(ONLY|LAST)' $t}); 39 | } 40 | # p: prints a string to STDOUT appending \n 41 | # w: prints a string to STDERR appending \n 42 | # Both respect the $silent value 43 | sub p { print "$_[0]\n" if (!$silent) } 44 | sub w { warn "$_[0]\n" if (!$silent) } 45 | 46 | # blank: runs a command and looks at the output. If the output is not 47 | # blank it is printed (and the program dies if stop_on_error is 1) 48 | sub blank { 49 | my ($command) = @_; 50 | if ($stop_on_error) { 51 | my $output = `$command`; 52 | if ($output ne '') { 53 | die $output; 54 | } 55 | } else { 56 | system($command); 57 | } 58 | } 59 | 60 | my $version; 61 | sub process_file { 62 | my $file = shift; 63 | # Check the sanity of each .lua file 64 | open my $in, $file or 65 | die "ERROR: Can't open $file for reading: $!\n"; 66 | my $found_ver; 67 | while (<$in>) { 68 | my ($ver, $skipping); 69 | if (/(?x) (?:_VERSION|version) \s* = .*? ([\d\.]*\d+) (.*? SKIP)?/) { 70 | my $orig_ver = $ver = $1; 71 | $found_ver = 1; 72 | $skipping = $2; 73 | $ver =~ s{^(\d+)\.(\d{3})(\d{3})$}{join '.', int($1), int($2), int($3)}e; 74 | w("$file: $orig_ver ($ver)"); 75 | last; 76 | 77 | } elsif (/(?x) (?:_VERSION|version) \s* = \s* ([a-zA-Z_]\S*)/) { 78 | w("$file: $1"); 79 | $found_ver = 1; 80 | last; 81 | } 82 | 83 | if ($ver and $version and !$skipping) { 84 | if ($version ne $ver) { 85 | die "$file: $ver != $version\n"; 86 | } 87 | } elsif ($ver and !$version) { 88 | $version = $ver; 89 | } 90 | } 91 | if (!$found_ver) { 92 | w("WARNING: No \"_VERSION\" or \"version\" field found in `$file`."); 93 | } 94 | close $in; 95 | 96 | p("Checking use of Lua global variables in file $file..."); 97 | p("\top no.\tline\tinstruction\targs\t; code"); 98 | blank("luac -p -l $file | grep -E '[GS]ETGLOBAL' | grep -vE '\\<(require|type|tostring|error|ngx|ndk|jit|setmetatable|getmetatable|string|table|io|os|print|tonumber|math|pcall|xpcall|unpack|pairs|ipairs|assert|module|package|coroutine|[gs]etfenv|next|rawget|rawset|rawlen|select)\\>'"); 99 | unless ($no_long_line_check) { 100 | p("Checking line length exceeding 80..."); 101 | blank("grep -H -n -E --color '.{81}' $file"); 102 | } 103 | } -------------------------------------------------------------------------------- /vendor/Util.lua: -------------------------------------------------------------------------------- 1 | --工具方法,网络收集 2 | --Copyright (C) Yuanlun He. 3 | -- http://en.wikipedia.org/wiki/Trim_(programming) 4 | 5 | local setmetatable = setmetatable 6 | local require = require 7 | local pcall = pcall 8 | local pairs = pairs 9 | local type = type 10 | local floor = math.floor 11 | local tonumber = tonumber 12 | local gsub = string.gsub 13 | local find = string.find 14 | local sub = string.sub 15 | local gsub = string.gsub 16 | local string_char = string.char 17 | local ngx_header = ngx.header 18 | local ngx_cookie_time = ngx.cookie_time 19 | local ngx_time= ngx.time 20 | local table_insert = table.insert 21 | local escape = ndk.set_var.set_quote_sql_str 22 | 23 | local json = require "cjson" 24 | local aes = require "resty.aes" 25 | local resty_str = require "resty.string" 26 | 27 | local _M = {_VERSION='0.01'} 28 | 29 | function _M.trim(s) 30 | return (s:gsub("^%s*(.-)%s*$", "%1")) 31 | end 32 | 33 | function _M.ltrim(s) 34 | return (s:gsub("^%s*", "")) 35 | end 36 | 37 | function _M.rtrim(s) 38 | local n = #s 39 | while n > 0 and s:find("^%s", n) do n = n - 1 end 40 | return s:sub(1, n) 41 | end 42 | 43 | -- 字符串 split 分割 44 | function _M.split(s, p) 45 | local rt= {} 46 | gsub(s, '[^'..p..']+', function(w) table_insert(rt, w) end ) 47 | return rt 48 | end 49 | 50 | --字符串转整数 51 | function _M.intval(str) 52 | return floor(tonumber(str) or 0) 53 | end 54 | 55 | --转义sql参数 56 | function _M.mescape(val) 57 | return val and escape(val) or '' 58 | end 59 | 60 | --检查变量是否为空 61 | function _M.empty(val) 62 | return val==nil or (type(val)=='string' and val=='') 63 | or (type(val)=='number' and val==0) 64 | or (type(val)=='table' and next(val)==nil ) 65 | or (type(val)=='boolean' and val==false ) 66 | end 67 | 68 | --检查值是否在一个表里 69 | function _M.in_array(val,arr) 70 | for _,v in pairs(arr) do 71 | if v==val then return true end 72 | end 73 | return false 74 | end 75 | 76 | --字符串转json 77 | function _M.json_decode( str ) 78 | if not str then return nil end 79 | local json_value = nil 80 | pcall(function (str) json_value = json.decode(str) end, str) 81 | return json_value 82 | end 83 | 84 | --json转字符串 85 | function _M.json_encode( obj ) 86 | if not obj then return nil end 87 | local str = nil 88 | pcall(function (obj) str = json.encode(obj) end, obj) 89 | return str 90 | end 91 | 92 | local AES_KEY = "My_Key_For_AES-256-CBC" 93 | local AES_SALT = "My_Salt" 94 | 95 | --AES-256-CBC解密 96 | function _M.aes_decrypt(str,key,salt) 97 | key = key or AES_KEY 98 | salt = salt or AES_SALT 99 | local aesn = aes:new(key, salt, aes.cipher(256,"cbc"), aes.hash.sha512, 5) 100 | str = str:gsub('..', function (cc) return string_char(tonumber(cc, 16)) end) 101 | return aesn:decrypt(str) 102 | end 103 | 104 | --AES-256-CBC加密 105 | function _M.aes_encrypt(str,key,salt) 106 | key = key or AES_KEY 107 | salt = salt or AES_SALT 108 | local aesn = aes:new(key, salt, aes.cipher(256,"cbc"), aes.hash.sha512, 5) 109 | return resty_str.to_hex(aesn:encrypt(str)) 110 | end 111 | 112 | --设置cookie 113 | function _M.set_cookie(name,value,expire,path,domain,secure,httponly) 114 | if not name then return end 115 | 116 | local cookies = ngx_header['Set-Cookie'] or {} 117 | cookies = type(cookies)=='table' and cookies or {cookies} 118 | 119 | expire = (value==nil or value=='') and -3600 or expire --删除cookie 120 | 121 | cookies[#cookies+1] = name..'='..(value or '') 122 | ..(expire and ';Expires='..ngx_cookie_time(ngx_time()+expire) or '') 123 | ..(path and ';Path='..path or '') 124 | ..(domain and ';Domain='..domain or '') 125 | ..(secure and ';Secure' or '') 126 | ..(httponly and ';Httponly' or ''); 127 | 128 | ngx.header["Set-Cookie"] = cookies 129 | return cookies 130 | end 131 | 132 | return _M 133 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /vendor/resty/template/microbenchmark.lua: -------------------------------------------------------------------------------- 1 | local template = require "resty.template" 2 | 3 | local ok, new_tab = pcall(require, "table.new") 4 | if not ok then 5 | new_tab = function() return {} end 6 | end 7 | 8 | local function run(iterations) 9 | local gc, total, print, parse, compile, iterations, clock, format = collectgarbage, 0, ngx and ngx.say or print, template.parse, template.compile, iterations or 1000, os.clock, string.format 10 | local view = [[ 11 | ]] 16 | 17 | print(format("Running %d iterations in each test", iterations)) 18 | 19 | gc() 20 | gc() 21 | 22 | local x = clock() 23 | for _ = 1, iterations do 24 | parse(view, true) 25 | end 26 | local z = clock() - x 27 | print(format(" Parsing Time: %.6f", z)) 28 | total = total + z 29 | 30 | gc() 31 | gc() 32 | 33 | x = clock() 34 | for _ = 1, iterations do 35 | compile(view, nil, true) 36 | template.cache = {} 37 | end 38 | z = clock() - x 39 | print(format("Compilation Time: %.6f (template)", z)) 40 | total = total + z 41 | 42 | compile(view, nil, true) 43 | 44 | gc() 45 | gc() 46 | 47 | x = clock() 48 | for _ = 1, iterations do 49 | compile(view, 1, true) 50 | end 51 | z = clock() - x 52 | print(format("Compilation Time: %.6f (template, cached)", z)) 53 | total = total + z 54 | 55 | local context = { "Emma", "James", "Nicholas", "Mary" } 56 | 57 | template.cache = {} 58 | 59 | gc() 60 | gc() 61 | 62 | x = clock() 63 | for _ = 1, iterations do 64 | compile(view, 1, true)(context) 65 | template.cache = {} 66 | end 67 | z = clock() - x 68 | print(format(" Execution Time: %.6f (same template)", z)) 69 | total = total + z 70 | 71 | template.cache = {} 72 | compile(view, 1, true) 73 | 74 | gc() 75 | gc() 76 | 77 | x = clock() 78 | for _ = 1, iterations do 79 | compile(view, 1, true)(context) 80 | end 81 | z = clock() - x 82 | print(format(" Execution Time: %.6f (same template, cached)", z)) 83 | total = total + z 84 | 85 | template.cache = {} 86 | 87 | local views = new_tab(iterations, 0) 88 | for i = 1, iterations do 89 | views[i] = "

Iteration " .. i .. "

\n" .. view 90 | end 91 | 92 | gc() 93 | gc() 94 | 95 | x = clock() 96 | for i = 1, iterations do 97 | compile(views[i], i, true)(context) 98 | end 99 | z = clock() - x 100 | print(format(" Execution Time: %.6f (different template)", z)) 101 | total = total + z 102 | 103 | gc() 104 | gc() 105 | 106 | x = clock() 107 | for i = 1, iterations do 108 | compile(views[i], i, true)(context) 109 | end 110 | z = clock() - x 111 | print(format(" Execution Time: %.6f (different template, cached)", z)) 112 | total = total + z 113 | 114 | local contexts = new_tab(iterations, 0) 115 | 116 | for i = 1, iterations do 117 | contexts[i] = { "Emma", "James", "Nicholas", "Mary" } 118 | end 119 | 120 | template.cache = {} 121 | 122 | gc() 123 | gc() 124 | 125 | x = clock() 126 | for i = 1, iterations do 127 | compile(views[i], i, true)(contexts[i]) 128 | end 129 | z = clock() - x 130 | print(format(" Execution Time: %.6f (different template, different context)", z)) 131 | total = total + z 132 | 133 | gc() 134 | gc() 135 | 136 | x = clock() 137 | for i = 1, iterations do 138 | compile(views[i], i, true)(contexts[i]) 139 | end 140 | z = clock() - x 141 | print(format(" Execution Time: %.6f (different template, different context, cached)", z)) 142 | total = total + z 143 | print(format(" Total Time: %.6f", total)) 144 | end 145 | 146 | return { 147 | run = run 148 | } -------------------------------------------------------------------------------- /runtime/mime.types: -------------------------------------------------------------------------------- 1 | 2 | types { 3 | text/html html htm shtml; 4 | text/css css; 5 | text/xml xml; 6 | image/gif gif; 7 | image/jpeg jpeg jpg; 8 | application/javascript js; 9 | application/atom+xml atom; 10 | application/rss+xml rss; 11 | 12 | text/mathml mml; 13 | text/plain txt; 14 | text/vnd.sun.j2me.app-descriptor jad; 15 | text/vnd.wap.wml wml; 16 | text/x-component htc; 17 | 18 | image/png png; 19 | image/tiff tif tiff; 20 | image/vnd.wap.wbmp wbmp; 21 | image/x-icon ico; 22 | image/x-jng jng; 23 | image/x-ms-bmp bmp; 24 | image/svg+xml svg svgz; 25 | image/webp webp; 26 | 27 | application/font-woff woff; 28 | application/java-archive jar war ear; 29 | application/json json; 30 | application/mac-binhex40 hqx; 31 | application/msword doc; 32 | application/pdf pdf; 33 | application/postscript ps eps ai; 34 | application/rtf rtf; 35 | application/vnd.apple.mpegurl m3u8; 36 | application/vnd.ms-excel xls; 37 | application/vnd.ms-fontobject eot; 38 | application/vnd.ms-powerpoint ppt; 39 | application/vnd.wap.wmlc wmlc; 40 | application/vnd.google-earth.kml+xml kml; 41 | application/vnd.google-earth.kmz kmz; 42 | application/x-7z-compressed 7z; 43 | application/x-cocoa cco; 44 | application/x-java-archive-diff jardiff; 45 | application/x-java-jnlp-file jnlp; 46 | application/x-makeself run; 47 | application/x-perl pl pm; 48 | application/x-pilot prc pdb; 49 | application/x-rar-compressed rar; 50 | application/x-redhat-package-manager rpm; 51 | application/x-sea sea; 52 | application/x-shockwave-flash swf; 53 | application/x-stuffit sit; 54 | application/x-tcl tcl tk; 55 | application/x-x509-ca-cert der pem crt; 56 | application/x-xpinstall xpi; 57 | application/xhtml+xml xhtml; 58 | application/xspf+xml xspf; 59 | application/zip zip; 60 | 61 | application/octet-stream bin exe dll; 62 | application/octet-stream deb; 63 | application/octet-stream dmg; 64 | application/octet-stream iso img; 65 | application/octet-stream msi msp msm; 66 | 67 | application/vnd.openxmlformats-officedocument.wordprocessingml.document docx; 68 | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx; 69 | application/vnd.openxmlformats-officedocument.presentationml.presentation pptx; 70 | 71 | audio/midi mid midi kar; 72 | audio/mpeg mp3; 73 | audio/ogg ogg; 74 | audio/x-m4a m4a; 75 | audio/x-realaudio ra; 76 | 77 | video/3gpp 3gpp 3gp; 78 | video/mp2t ts; 79 | video/mp4 mp4; 80 | video/mpeg mpeg mpg; 81 | video/quicktime mov; 82 | video/webm webm; 83 | video/x-flv flv; 84 | video/x-m4v m4v; 85 | video/x-mng mng; 86 | video/x-ms-asf asx asf; 87 | video/x-ms-wmv wmv; 88 | video/x-msvideo avi; 89 | } 90 | -------------------------------------------------------------------------------- /vendor/Query.lua: -------------------------------------------------------------------------------- 1 | --查询构建器 2 | --version:'0.0.1' 3 | --Copyright (C) Yuanlun He. 4 | 5 | local setmetatable = setmetatable 6 | local table_insert = table.insert 7 | local table_concat = table.concat 8 | local require = require 9 | local ipairs = ipairs 10 | local pairs = pairs 11 | local next = next 12 | local type = type 13 | 14 | local util = require("vendor.Util") 15 | local mescape = util.mescape 16 | local intval = util.intval 17 | local in_array = util.in_array 18 | 19 | return function(data) 20 | 21 | data = data or {} 22 | 23 | local _M = {_VERSION = '0.01'} 24 | local self = {feild = '*',cond = '1'} 25 | 26 | --返回私有字段 27 | function _M.get(key) 28 | return self[key] 29 | end 30 | 31 | --加载数据 32 | function _M.new(d) 33 | data = d and d or {} 34 | return setmetatable(data,{__index=_M}) 35 | end 36 | 37 | --设置要操作的库 38 | function _M.use(db) 39 | self.db = db 40 | return _M 41 | end 42 | 43 | --设置要操作的表 44 | function _M.from(tableName) 45 | self.table = tableName 46 | if not self.db then _M.use() end 47 | return _M 48 | end 49 | 50 | --设置select的字段 51 | function _M.select(feild) 52 | if type(feild)=='string' and feild~='' then 53 | self.feild = feild 54 | elseif type(feild)=='table' and next(feild) then 55 | self.feild = '`'..table_concat(feild,'`,`')..'`' 56 | else 57 | self.feild = '*' 58 | end 59 | return _M 60 | end 61 | 62 | --添加参数 63 | local function andWhere(cond) 64 | local res = '' 65 | local isstr = (type(cond[1])=='string') 66 | if isstr and (cond[1]=='and' or cond[1]=='or') then 67 | cond[2] = type(cond[2])=='table' and andWhere(cond[2]) or cond[2] 68 | cond[3] = type(cond[3])=='table' and andWhere(cond[3]) or cond[3] 69 | res = res..' ('..cond[2]..' '..cond[1]..' '..cond[3]..') ' 70 | return res 71 | end 72 | 73 | if isstr and (cond[1]=='between' or cond[1]=='not between') then 74 | res = res..' `'..cond[2]..'` '..cond[1]..' '..mescape(cond[3])..' AND '..mescape(cond[4])..' ' 75 | return res 76 | end 77 | 78 | if isstr and (cond[1]=='in' or cond[1]=='not in') then 79 | local t = {} 80 | for i,j in pairs(cond[3]) do t[i] = mescape(j) end 81 | res = res..' `'..cond[2]..'` '..cond[1]..' ('..table_concat(t,",") ..') ' 82 | return res 83 | end 84 | 85 | if isstr and in_array(cond[1],{'<','>','=','<=','>=','!=','=>','=<'}) then 86 | res = res..' `'..cond[2]..'` '..cond[1]..' '..mescape(cond[3]) 87 | return res 88 | end 89 | 90 | local t = {} 91 | for k,v in pairs(cond) do 92 | if type(v)=='table' and next(v) then 93 | if type(k)=='number' then 94 | table_insert(t,andWhere(v)) 95 | else 96 | for i,j in pairs(v) do v[i] = mescape(j) end 97 | v = table_concat(v,",") 98 | table_insert(t,'`'..k..'` IN ('..v..')') 99 | end 100 | else 101 | if type(k)=='number' then 102 | table_insert(t,v) 103 | else 104 | table_insert(t,'`'..k..'`='..mescape(v)) 105 | end 106 | end 107 | end 108 | res = '('..table_concat(t,') AND (')..')' 109 | 110 | return res 111 | end 112 | 113 | --设置where条件 114 | function _M.where(cond,params) 115 | if type(cond)=='string' and cond~='' then 116 | self.cond = cond 117 | _M:addParams(params) 118 | elseif type(cond)=='table' and next(cond) then 119 | self.cond = andWhere(cond) 120 | else 121 | self.cond = '1' 122 | end 123 | return _M 124 | end 125 | 126 | --添加查询参数 127 | function _M.addParams(params) 128 | if params and next(params) and self.cond then 129 | for k,v in pairs(params) do 130 | self.cond = self.cond:gsub(k,mescape(v)) 131 | end 132 | end 133 | return _M 134 | end 135 | 136 | --设置orderBy 137 | function _M.orderBy(orderby) 138 | if type(orderby)=='string' and orderby~='' then 139 | self.order = orderby 140 | elseif type(orderby)=='table' and next(orderby) then 141 | local t = {} 142 | for k,v in pairs(orderby) do table_insert(t,k..' '..v) end 143 | self.order = table_concat(t,',') 144 | else 145 | self.order = nil 146 | end 147 | return _M 148 | end 149 | 150 | --设置groupBy 151 | function _M.groupBy(groupby) 152 | if type(groupby)=='string' and groupby~='' then 153 | self.group = groupby 154 | elseif type(groupby)=='table' and next(groupby) then 155 | self.group = '`'..table_concat(groupby,'`,`')..'`' 156 | else 157 | self.group = nil 158 | end 159 | return _M 160 | end 161 | 162 | --设置offset 163 | function _M.offset(offset) 164 | offset = offset and intval(offset) or 0 165 | if offset > 0 then 166 | self.start = offset 167 | else 168 | self.start = nil 169 | end 170 | return _M 171 | end 172 | 173 | --设置limit 174 | function _M.limit(limit) 175 | limit = limit and intval(limit) or 0 176 | if limit > 0 then 177 | self.size = limit 178 | else 179 | self.size = nil 180 | end 181 | return _M 182 | end 183 | 184 | --设置分页查询 185 | function _M.page(page) 186 | page = page and page or {} 187 | if page.limit then self.size = page.limit end 188 | if page.offset then self.start = page.offset end 189 | return _M 190 | end 191 | 192 | --返回所有符合条件的结果集 193 | function _M.all() 194 | self.sql = 'SELECT '..self.feild 195 | ..' FROM '..self.table 196 | ..(self.cond and ' WHERE '..self.cond or '') 197 | ..(self.group and ' GROUP BY '..self.group or '') 198 | ..(self.order and ' ORDER BY '..self.order or '') 199 | ..(self.size and ' LIMIT '..self.size or '') 200 | ..(self.start and ' OFFSET '..self.start or ''); 201 | local res,err = self.db:query(self.sql) 202 | self.err = err 203 | return _M.new(res) 204 | end 205 | 206 | --返回结果集的第一行 207 | function _M.one() 208 | self.sql = 'SELECT '..self.feild 209 | ..' FROM '..self.table 210 | ..(self.cond and ' WHERE '..self.cond or '') 211 | ..(self.group and ' GROUP BY '..self.group or '') 212 | ..(self.order and ' ORDER BY '..self.order or '') 213 | ..' LIMIT 1'; 214 | local res,err = self.db:query(self.sql) 215 | self.err = err 216 | return _M.new(res and res[1] or {}) 217 | end 218 | 219 | --返回所有符合条件的条数 220 | function _M.count() 221 | local s = self.feild:find(',') and 'count(1)' or 'count('..self.feild..')' 222 | self.sql = 'SELECT '..s 223 | ..' FROM '..self.table 224 | ..(self.cond and ' WHERE '..self.cond or '') 225 | ..(self.group and ' GROUP BY '..self.group or '') 226 | ..(self.order and ' ORDER BY '..self.order or ''); 227 | local res,err = self.db:query(self.sql) 228 | self.err = err 229 | return res and intval(res[1][s]) or 0 230 | end 231 | 232 | --根据sql返回结果集 233 | function _M.sql(sql) 234 | self.sql = sql 235 | return self.db:query(self.sql) 236 | end 237 | 238 | --获取主键 239 | function _M.primaryKey() 240 | if not self.tableSchema then 241 | self.tableSchema = {} 242 | for i,v in ipairs(self.db:query('desc '..self.table)) do 243 | self.tableSchema[v.Field] = v 244 | if v.Key == "PRI" then self.primaryKey = v.Field end 245 | end 246 | end 247 | return self.primaryKey 248 | end 249 | 250 | --保存 251 | function _M.save(d) 252 | return self.sql and _M.update(d) or _M.insert(d) 253 | end 254 | 255 | --插入数据 256 | function _M.insert(d) 257 | data = d and _M.new(d) or data 258 | 259 | local keys,values = {},{} 260 | for k,v in pairs(data) do 261 | if in_array(type(v),{'string','number','boolean'}) then 262 | table_insert(keys,k) 263 | table_insert(values,mescape(v)) 264 | end 265 | end 266 | self.sql = 'INSERT INTO `'..self.table..'`' 267 | ..' (`'..table_concat(keys,'`,`')..'`)' 268 | ..' VALUES ('..table_concat(values,',')..')'; 269 | local res,err = self.db:query(self.sql) 270 | self.err = err 271 | --res = {"insert_id":0,"server_status":2,"warning_count":0,"affected_rows":1} 272 | 273 | if res then 274 | self.insert_id = res.insert_id 275 | self.affected_rows = res.affected_rows 276 | end 277 | 278 | return res and data or _M.new() 279 | end 280 | 281 | --更新数据 282 | function _M.update(d) 283 | 284 | data = d and _M.new(d) or data 285 | 286 | local t = {} 287 | for k,v in pairs(data) do 288 | table_insert(t,'`'..k..'`='..mescape(v)) 289 | end 290 | self.sql = 'UPDATE `'..self.table..'`' 291 | ..' SET '..table_concat(t,',') 292 | ..' WHERE '..(self.cond and self.cond or addWhere(data)); 293 | 294 | local res,err = self.db:query(self.sql) 295 | self.err = err 296 | 297 | if res then 298 | self.affected_rows = res.affected_rows 299 | end 300 | 301 | return res and data or _M.new() 302 | end 303 | 304 | --删除数据 305 | function _M.delete(d) 306 | data = d and _M.new(d) or data 307 | self.sql = 'DELETE FROM `'..self.table..'`' 308 | ..' WHERE '..(self.cond and self.cond or addWhere(data)); 309 | local res,err = self.db:query(self.sql) 310 | self.err = err 311 | --res={"insert_id":0,"server_status":2,"warning_count":0,"affected_rows":1} 312 | if res then 313 | self.affected_rows = res.affected_rows 314 | end 315 | 316 | return res and res.affected_rows or 0 317 | end 318 | 319 | return setmetatable(data,{__index=_M}) 320 | end 321 | -------------------------------------------------------------------------------- /views/site/guide.lua.html: -------------------------------------------------------------------------------- 1 | return [[ 2 | {% 3 | context.title = '开发说明' 4 | %} 5 |
6 |
应用结构
7 |
8 |

目录及文件:

9 |
 10 | |____index.lua 						#入口文件
 11 | |____lua-releng 					#检查代码质量/是否使用了全局变量
 12 | |____config 						#配置文件目录
 13 | | |____db.lua 						#数据库配置
 14 | | |____lang.lua 					#语言包配置
 15 | | |____memcache.lua 					#memcache配置
 16 | | |____session.lua 					#session配置
 17 | | |____web.lua 						#网站基本信息配置
 18 | |____controllers 					#controller目录
 19 | | |____site.lua 					#前端页面
 20 | |____models 						#models目录
 21 | | |____LoginForm.lua 					#登录逻辑处理类
 22 | | |____Userinfo.lua 					#用户信息管理类
 23 | |____runtime 						#Openresty运行目录
 24 | | |____client_body_temp 				#post上传的临时保存目录
 25 | | |____fastcgi_temp 					#fastcig临时目录
 26 | | |____logs 						#站点日志目录
 27 | | | |____access.log 					#请求日志
 28 | | | |____error.log 					#错误日志
 29 | | | |____nginx.pid 					#nginx进程文件
 30 | | |____nginx.conf 					#站点nginx配置文件
 31 | | |____proxy_temp 					#proxy_temp
 32 | | |____scgi_temp 					#scgi_temp 
 33 | | |____start.sh 					#站点启动程序
 34 | | |____stop.sh 						#站点停止程序
 35 | | |____uwsgi_temp 					#uwsgi_temp
 36 | |____vendor 						#框架及第三方类
 37 | | |____ActiveRecord.lua 				#数据库处理基类
 38 | | |____Application.lua 					#请求处理类
 39 | | |____Controller.lua 					#Controller基类
 40 | | |____Files.lua 					#上传文件接收类
 41 | | |____Memcache.lua 					#Memcache操作类
 42 | | |____Model.lua 					#Model基类
 43 | | |____Mysql.lua 					#Mysql操作类
 44 | | |____Pager.lua 					#分页类
 45 | | |____Query.lua 					#查询构建器
 46 | | |____Request.lua 					#请求信息工具类
 47 | | |____resty 						#第三方resty工具
 48 | | | |____http.lua 					#http请求工具
 49 | | | |____template.lua 					#lua模版工具类
 50 | | |____Session.lua 					#Session操作类
 51 | | |____User.lua 					#用户信息操作类
 52 | | |____Util.lua 					#基本工具类
 53 | |____views 						#页面模版目录
 54 | | |____layout 						#页面框架目录
 55 | | | |____main.lua.html 					#基本站点框架
 56 | | |____site 						#站点页面目录
 57 | | | |____error.lua.html 				#错误信息页
 58 | | | |____guide.lua.html 				#开发说明页
 59 | | | |____index.lua.html 				#首页
 60 | | | |____login.lua.html 				#登录页
 61 | |____web 						#静态资源目录
 62 | | |____css 						#样式
 63 | | | |____bootstrap-theme.css 				#bootstrap
 64 | | | |____bootstrap-theme.min.css 			#bootstrap
 65 | | | |____bootstrap.css 					#bootstrap
 66 | | | |____bootstrap.min.css 				#bootstrap
 67 | | | |____site.css 					#站点样式
 68 | | |____js  						#javascript	
 69 | | | |____bootstrap.js 					#bootstrap
 70 | | | |____bootstrap.min.js 				#bootstrap
 71 | | | |____jquery.js 					#jquery
 72 | | | |____jquery.min.js 					#jquery
 73 | | | |____jquery.slim.js 				#jquery
 74 | | | |____jquery.slim.min.js 				#jquery
 75 | | |____favicon.ico 					#图标
 76 | | |____robots.txt 					#robots.txt
 77 | 
78 |

运行机制概述

79 |
 80 | 每一次应用开始处理 HTTP 请求时,它都会进行一个近似的流程。
 81 | 
 82 | 用户提交指向 入口脚本 index.lua 的请求
 83 | 入口脚本会创建一个 应用(Application) 实例用于处理该请求,并加载 配置。
 84 | 应用会通过 request(请求) 应用组件解析被请求的 路由。
 85 | 应用创建一个 controller(控制器) 实例具体处理请求。
 86 | 执行controller中的before()方法进行请求过滤
 87 | 如果执行before()返回true,则继续执行 action(动作),否则终止 
 88 | 动作会加载一个数据模型,一般是从数据库中加载。
 89 | 动作会渲染一个 View(视图),并为其提供所需的数据模型。
 90 | 渲染得到的结果会返回给 response(响应) 应用组件。
 91 | 响应组件会把渲染结果发回给用户的浏览器。
 92 | 
93 | 94 |

调试模式:

95 |
 96 | 访问 /lua/index 会使用lua_code_cache off的模式访问站点
 97 | 访问 /lua/<filepath> 可调试对应的lua脚本
 98 | 
99 | 100 |

应用(Application)

101 |
102 | 每接收到一个 HTTP 请求,入口脚本 index.lua 会创建一个 应用(Application) 实例用于处理该请求
103 | Application实例创建后,会保存覆盖全局变量ngx.ctx(此全局变量生命周期对应每个请求)
104 | 
105 | ngx.ctx = require("vendor.Application"):new()
106 | 
107 | Application在创建时,会加载网站的基本配置及基本工具作为属性,如:
108 | 
109 | ngx.ctx.web 相当于 require("config.web"),	 --站点配置信息表
110 | ngx.ctx.lang 相当于 require("config.lang"),	 --语言包配置信息
111 | ngx.ctx.session 相当于 require("config.session"), --站点Session类
112 | ngx.ctx.user 相当于 require("vendor.User"),	 --站点用户工具类
113 | ngx.ctx.request 相当于 require("vendor.Request"), --站点请求处理类
114 | 
115 | 在执行Application的new()时,会创建一个继承于Application的Controller实例,
116 | 因此ngx.ctx即是一个Application也是一个Controller实例,并且controller中也可以通过
117 | self.web,self.lang,self.session.self.user.self.request直接引用以上配置及工具
118 | 
119 | 注意:在使用时,尽量少使用全局变量ngx.ctx,而应该通过函数传值的方式进行传递,能获得更快的运行速度
120 | 
121 |

控制器(Controllers):

122 |
123 | local _M = require("vendor.Controller"):new{_VERSION='0.01'}	--生成Controller新实例
124 | 
125 | function _M:indexAction()	--动作方法名必须以name+Action组成
126 | 	if not self.user.isLogin then return self:loginAction() end		--使用用户信息类判断用户是否登录
127 | 
128 | 	local Product = require('models.Product')  				--使用Product数据表操作类
129 | 	local query = Product.find().where{'in','product_id',self.user.resource} --fin()方法生成新的查询器
130 | 
131 | 	local page = Pager{				--使用分页类
132 | 		pageSize = 8, 				--设置每页显示最多8条信息
133 | 		totalCount = query.count(),		--使用查询器查询符合条件的数据总条数
134 | 	}
135 | 
136 | 	local list = query.orderBy('create_time desc').page(page).all() --使用查询器查询分页数据集
137 | 	
138 | 	return self:render('index',{
139 | 		page = page,
140 | 		list = list,
141 | 	})
142 | end
143 | 
144 | retrun _M
145 | 
146 | 如果保存为controllers/filename.lua,则访问?act=filename.index时,会执行上面对应的indexAction()方法
147 | 
148 |

模型(Models)

149 |
150 | 模型是 MVC 模式中的一部分, 是代表业务数据、规则和逻辑的对象。
151 | 可通过继承 "vendor.Model" 或它的子类定义模型类, 基类"vendor.Model"支持许多实用的特性,如:
152 | 
153 | local _M = require("vendor.Model"):new{_version='0.0.1'}	--生成Model新实例
154 | 
155 | local Userinfo = require('models.Userinfo')		--在Model里使用其他model
156 | 
157 | function _M.rules()		--方法rules添加数据校验规则
158 | 	return {
159 | 		{{'username','password','sacode'}, 'trim'},	--通过trim自动过滤输入
160 | 		{'username', 'required',message='请填写登录账号'},	--通过required规则设置必须填写
161 | 		--{'username', 'email'}, 			--如果需要用户名必须为email时设置
162 | 		{'password', 'required',message='请填写登录密码'},
163 | 		--使用自定义方法校验参数
164 | 		{'password','checkPass'}		--使用自定义checkPass方法进行校验
165 | 	}
166 | end
167 | 
168 | function _M:checkPass(key)
169 | 	if self:hasErrors() then return end
170 | 
171 | 	local user = Userinfo.getUserByName(self.username)
172 | 	if not user then 
173 | 		self:addError('username','账号不存在')
174 | 	elseif user.password ~= self.password then
175 | 		self:addError('password','密码错误')
176 | 	else
177 | 		self.userinfo = user
178 | 	end
179 | end
180 | 
181 | function _M:login(user)
182 | 	if not self:validate() then return false end
183 | 	
184 | 	return user.login(self.userinfo, rememberMe and 3600*24*30 or 0)
185 | end
186 | 
187 | return _M
188 | 
189 | 
190 | 在Controller里使用Model:
191 | 
192 | local model = require('models.LoginForm'):new()		--因为Model包含有数据,一定要调用new方法生成新实例,避免出现数据缓存问题
193 | 
194 | --通过model的load()方法,可自动装载数据
195 | if model:load(self.request.post()) and model:login(self.user) then
196 | 	return self:goHome()
197 | end
198 | 
199 |

视图(Views)

200 |
201 | 视图基于lua-resty-template(https://github.com/bungle/lua-resty-template)实现
202 | 
203 | 在视图中必须使用以下标签:
204 | {{expression}}, 输出expression表达式,并用经过html格式化
205 | {*expression*}, 原样输出expression表达式的结果
206 | {% lua code %}, 执行Lua代码
207 | {# comments #}, 所有{#和#}之间的内容都被认为是注释掉的(即不输出或执行)
208 | 
209 | 与lua-resty-template的普通用法不同,所有视图文件,以lua文件的形式保存在views子目录下,文件名为*.lua.html
210 | controller中的render方法渲染试图时,会以require的形式去获取视图内容
211 | 
212 | views/layout 目录存放框架视图,渲染视图时默认使用views/layout/main.lua.html,
213 | controller中可通过layout属性设置使用不同的框架视图,如:
214 | local _M = require("vendor.Controller"):new{layout = 'manage'}
215 | 
216 | views下的其他子目录分别为不同功能模块对应的内容视图,所有页头,页尾,菜单等内容应在框架视图中实现
217 | 内容视图中不推荐使用{(template)}的形式来包含其他视图,因与默认渲染方式不同,会导致找不到文件等错误
218 | 
219 | controller在渲染视图时,会将所有application及controller的数据传递给视图,因此在视图中可以直接使用这些数据
220 | 如:{{lang.siteName}}可输出语言包中配置的网站名称
221 | 
222 | 视图中可以通过设置context属性的方式用于内容视图跟框架视图之间传值,如设置:
223 | {% context.title = '开发说明' %}
224 | 则可以在框架视图中{{title}}输出显示
225 | 
226 |
227 |
请求处理
228 |
229 |

获取请求参数

230 |
231 | local request = require("vendor.Request")
232 | // 在Controller方法里可以直接通过self.request调用
233 | 
234 | local get = request.get()
235 | // 等价于php: $get = $_GET;
236 | 
237 | local id = request.get('id');   
238 | // 等价于php: $id = isset($_GET['id']) ? $_GET['id'] : null;
239 | 
240 | local id = request.get('id', 1)   
241 | // 等价于php: $id = isset($_GET['id']) ? $_GET['id'] : 1;
242 | 
243 | local post = request.post()
244 | // 等价于php: $post = $_POST;
245 | 
246 | local name = request.post('name')   
247 | // 等价于php: $name = isset($_POST['name']) ? $_POST['name'] : null;
248 | 
249 | local name = request.post('name', '')   
250 | // 等价于php: $name = isset($_POST['name']) ? $_POST['name'] : '';
251 | 
252 | local cookie = request.cookie()
253 | // 等价于php: $cookie = $_COOKIE;
254 | 
255 | local sso = request.cookie('sso')   
256 | // 等价于php: $sso = isset($_COOKIE['sso']) ? $_COOKIE['sso'] : null;
257 | 
258 | local sso = request.cookie('sso', '')   
259 | // 等价于php: $sso = isset($_COOKIE['sso']) ? $_COOKIE['sso'] : '';
260 | 
261 | --判断是否有上传文件
262 | ngx.say(request.isPostFile())
263 | // 注意跟php不同的地方,如果有上传文件时,普通参数是无法通过request.post()方法获取到的
264 | // 建议上传文件时,普通参数通过GET传递
265 | 
266 | --接收上传文件
267 | local file = require('vendor.Files')
268 | local savename = path..filename
269 | 
270 | local ok,err = file.receive('upfile',savename)	--接收name为upfile的上传文件
271 | if not ok  then  
272 | 	return {retcode=1,retmsg='接收文件失败,请重试:'..err}  
273 | end
274 | 
275 | 
276 | 277 |

设置cookie:

278 |
279 | local util = require "vendor.Util"
280 | util.set_cookie('abc','123')
281 | util.set_cookie('def','456',3600)
282 | util.set_cookie(name,value,expire,path,domain,secure,httponly)
283 | 
284 | 285 |

Session操作:

286 |
287 | local session = require "vendor.Session"
288 | // 在Controller方法里可以直接通过self.session调用
289 | --session.start()	--启用session,会去设置一个_sessionid的cookie,此操作可以省略
290 | --session.exptime = 1800 --修改session失效时间时,默认30分钟
291 | --session.cache = ngx_shared['session_cache'] --默认使用ngx的缓存,对应runtime/nginx.conf里的lua_shared_dict配置项
292 | --session.cache = require("vendor.Memcache"):new{host="127.0.0.1",port="11211"} --使用memcached缓存打开此配置
293 | 
294 | local sessions = session.get()
295 | // 等价于php: $sessions = $_SESSION;
296 | 
297 | local abc = session.get('abc')   
298 | --等价于:local abc = session.abc 
299 | --等价于:local abc = session['abc']
300 | // 等价于php: $abc = isset($_SESSION['abc']) ? $_SESSION['abc'] : null;
301 | 
302 | local abc = session.get('abc', '')   
303 | // 等价于php: $abc = isset($_SESSION['abc']) ? $_SESSION['abc'] : '';
304 | 
305 | //设置session值
306 | session.set('abc','abc-value')
307 | --等价于:session.abc = 'abc-value'
308 | --等价于:session['abc'] = 'abc-value'
309 | 
310 |
311 |
数据库操作(Working with Databases)
312 |
313 |

数据库访问 (DAO)

314 |
315 | local db = require('vendor.Mysql'):new{
316 |     host = "127.0.0.1",
317 |     port = 3306,
318 |     database = "mytest",
319 |     user = "root",
320 |     password = "",
321 |     charset = "utf8", --建议配置Mysql的默认连接字符集为utf8,可去掉此配置项,此项配置会增加一次'SET NAMES utf8'的操作
322 | }
323 | 
324 | 或者直接通过配置文件获得:
325 | local db = require('config.db')
326 | 
327 | 执行sql:
328 | local rows = db:query('select * from mytable')
329 | ngx.say(#rows)
330 | 
331 |

使用查询生成器 (Query Builder)

332 |
333 | vendor.Query 封装了 vendor.Mysql,并提供快捷构建 安全 查询语句的方法,例如:
334 | 
335 | local db = require('config.db')
336 | local query = require('vendor.Query')()
337 | local rows = query.use(db).from('products').where({product_id=123}).all()
338 | --相当于执行local rows = db:query("select * from products where product_id='123'")
339 | ngx.say(query.get('sql')) --能获取到构造出的sql语句
340 | 
341 | 
342 | --query.use()用于指定链接的数据库
343 | query.use(require('config.db'))
344 | 
345 | 
346 | --query.select()用于指定要查询的字段,不指定默认为select *
347 | query.select({'id', 'email'})
348 | // 等同于:
349 | query.select('id, email')
350 | 
351 | 
352 | --query.from()用于指定要查询的表格
353 | // SELECT * FROM `user`
354 | query.from('user')
355 | 
356 | 
357 | --query.where()用于定义 SQL 语句当中的 WHERE 子句。 你可以使用如下三种格式来定义 WHERE 条件:
358 | 
359 | 字符串格式,例如:'status=1' , 这个方法不会自动加引号或者转义。
360 | 哈希格式,例如: {status=1,type=2}  ,这个方法将正确地为字段名加引号以及为取值范围转义
361 | 操作符格式,例如:{'in', {'2012','2011'}}
362 | 
363 | 操作符格式
364 | 
365 | 操作符格式允许你指定类程序风格的任意条件语句,如下所示:
366 | 
367 | {操作符, 操作数1, 操作数2, ...}
368 | 其中每个操作数可以是字符串格式、哈希格式或者嵌套的操作符格式, 而操作符可以是如下列表中的一个:
369 | 
370 | and: 操作数会被 AND 关键字串联起来。例如,{'and', 'id=1', 'id=2'} 将会生成 id=1 AND id=2。
371 | 如果操作数是一个数组,它也会按上述规则转换成 字符串。例如,{'and', 'type=1', {'or', 'id=1', 'id=2'}} 
372 | 将会生成 type=1 AND (id=1 OR id=2)。 这个方法不会自动加引号或者转义。
373 | 
374 | or: 用法和 and 操作符类似,这里就不再赘述。
375 | 
376 | between: 第一个操作数为字段名称,第二个和第三个操作数代表的是这个字段 的取值范围。例如,{'between', 'id', 1, 10} 将会生成 id BETWEEN 1 AND 10。
377 | 
378 | not between: 用法跟between类似
379 | 
380 | in: 第一个操作数应为字段名称,第二个操作符既是一个数组。 例如, 
381 | {'in', 'id', {1, 2, 3}} 将生成 `id` IN ('1', '2', '3') 
382 | 该方法将正确地为字段名加引号以及为取值范围转义
383 | 
384 | not in: 用法和 in 操作符类似,这里就不再赘述。
385 | 
386 | --query.orderBy() 方法是用来指定 SQL 语句当中的 ORDER BY 子句的。例如,
387 | // ... ORDER BY create_time desc
388 | query.orderBy('create_time desc')
389 | 等价于:query.orderBy{create_time=desc}
390 | 
391 | 
392 | --query.groupBy() 方法是用来指定 SQL 语句当中的 GROUP BY 片断的。例如,
393 | // ... GROUP BY `id`, `status`
394 | query.groupBy{'id', 'status'}
395 | 
396 | 
397 | --query.limit() 和 query.offset() 是用来指定 SQL 语句当中 的 LIMIT 和 OFFSET 子句的。例如,
398 | // ... LIMIT 10 OFFSET 20   等价于mysql的limit 20,10
399 | query.limit(10).offset(20)
400 | 如果你指定了一个无效的 limit 或者 offset(例如,一个负数),那么它将会被忽略掉。
401 | 
402 | 
403 | --query.page() 能通过传递"vendor.Pager"对象,设置 LIMIT 和 OFFSET 达到分页查询的目的:
404 | local page = require("vendor.Pager"){
405 | 	pageSize = 8,
406 | 	totalCount = 10,
407 | }
408 | query.page(page) --等价于:query.limit(page.limit).offset(page.offset)
409 | 
410 | 
411 | 查询方法
412 | 
413 | vendor.Query 提供了一整套的用于不同查询目的的方法。
414 | 
415 | query.all(): 将返回一个由行组成的数组。
416 | query.one(): 返回结果集的第一行。
417 | query.count(): 返回 COUNT 查询的结果。
418 | 
419 | 例如:
420 | local db = require('config.db')
421 | local query = require('vendor.Query'){}.use(db).from('products').where{product_id=123}
422 | local rows = query.all()
423 | local row = query.one()
424 | local count = query.count()
425 | 
426 | 
427 | 其他方法:
428 | query.insert(): 插入数据。
429 | query.update(): 更新数据。
430 | query.delete(): 删除数据。
431 | query.save(): 新数据调用等价于query.insert(),查询结果集调用等价于query.update()。
432 | 
433 | 例如:
434 | local db = require('config.db')
435 | local data = {product_id=123,product_name='name123'}
436 | local query = require('vendor.Query')(data).use(db).from('products')
437 | 
438 | --插入:
439 | local row = query.insert()
440 | --要获取新插入的数据的自增长id,请使用:
441 | local id = row.get('insert_id') --等价于query.get('insert_id')
442 | 
443 | --更新:
444 | row.product_name = 'name456'
445 | row.update()	--或者query.update(row)
446 | 
447 | --删除:
448 | row.delete()	--或者query.update(row)
449 | 
450 |

使用活动记录 (Active Record)

451 |
452 | vendor.ActiveRecord 进一步封装了查询生成器vendor.Query
453 | 同时vendor.ActiveRecord继承于vendor.Model,能够使用模型对象的load(),rules(),hasErrors(),validate()等方法
454 | 
455 | 使用示例:
456 | local Product = require('vendor.ActiveRecord'){
457 | 	
458 | 	--db = require('config.db'),	--可选属性,指定使用的数据库连接,未设置时默认使用'config.db'
459 | 
460 | 	tableName = function()			--必须实现tableName方法,返回要操作的数据表名称
461 | 		return 'products'
462 | 	end
463 | 
464 | }
465 | 
466 | --插入数据:
467 | local product = Product.new{
468 | 	product_id	 = 123,
469 | 	product_name = 'name123',
470 | }	--返回的对象支持使用vendor.Query方法
471 | product.save()  --等价于product.insert()
472 | 
473 | --快速查找一行
474 | local row = Product.findOne{product_id = 123}	--返回的对象支持使用vendor.Query方法
475 | 
476 | --快速查找多行
477 | local rows = Product.findAll{user='creater'}	--返回的对象支持使用vendor.Query方法
478 | 
479 | --复杂查询
480 | local query = Product.find().where{user='creater'}	--返回的对象支持使用vendor.Query方法
481 | local page = Pager{
482 | 	pageSize = 8,
483 | 	totalCount = query.count(),
484 | }
485 | local list = query.orderBy('create_time desc').page(page).all()
486 | 
487 | --同样可以执行以下操作
488 | query.insert(): 插入数据。
489 | query.update(): 更新数据。
490 | query.delete(): 删除数据。
491 | 
492 | 
493 | 建议每个数据表格在models目录下建立一个继承于'vendor.ActiveRecord'的操作类
494 | 
495 |

数据库安全问题

496 |
497 | 使用vendor.ActiveRecord 或 vendor.Query	自动生成查询语句
498 | 在构造sql语句的过程中,如果传递的参数是表格,构造器会进行转义操作,防止sql注入
499 | 
500 | 但如果传递的是字符串,则不会进行转义操作,建议尽量少用,并能确保sql安全
501 | 可以通过 query.get('sql') 获取执行的sql语句
502 | 
503 | 如果要进行转义,可使用util.mescape()方法
504 | local util = require("vendor.Util")
505 | value = util.mescape(value)
506 | 
507 |
508 |
509 | 510 | 511 | 512 | ]] -------------------------------------------------------------------------------- /vendor/resty/template.lua: -------------------------------------------------------------------------------- 1 | local setmetatable = setmetatable 2 | local loadstring = loadstring 3 | local loadchunk 4 | local tostring = tostring 5 | local setfenv = setfenv 6 | local require = require 7 | local capture 8 | local concat = table.concat 9 | local assert = assert 10 | local prefix 11 | local write = io.write 12 | local pcall = pcall 13 | local phase 14 | local open = io.open 15 | local load = load 16 | local type = type 17 | local dump = string.dump 18 | local find = string.find 19 | local gsub = string.gsub 20 | local byte = string.byte 21 | local null 22 | local sub = string.sub 23 | local ngx = ngx 24 | local jit = jit 25 | local var 26 | 27 | local _VERSION = _VERSION 28 | local _ENV = _ENV 29 | local _G = _G 30 | 31 | local HTML_ENTITIES = { 32 | ["&"] = "&", 33 | ["<"] = "<", 34 | [">"] = ">", 35 | ['"'] = """, 36 | ["'"] = "'", 37 | ["/"] = "/" 38 | } 39 | 40 | local CODE_ENTITIES = { 41 | ["{"] = "{", 42 | ["}"] = "}", 43 | ["&"] = "&", 44 | ["<"] = "<", 45 | [">"] = ">", 46 | ['"'] = """, 47 | ["'"] = "'", 48 | ["/"] = "/" 49 | } 50 | 51 | local VAR_PHASES 52 | 53 | local ok, newtab = pcall(require, "table.new") 54 | if not ok then newtab = function() return {} end end 55 | 56 | local caching = true 57 | local template = newtab(0, 12) 58 | 59 | template._VERSION = "1.8" 60 | template.cache = {} 61 | 62 | local function enabled(val) 63 | if val == nil then return true end 64 | return val == true or (val == "1" or val == "true" or val == "on") 65 | end 66 | 67 | local function trim(s) 68 | return gsub(gsub(s, "^%s+", ""), "%s+$", "") 69 | end 70 | 71 | local function rpos(view, s) 72 | while s > 0 do 73 | local c = sub(view, s, s) 74 | if c == " " or c == "\t" or c == "\0" or c == "\x0B" then 75 | s = s - 1 76 | else 77 | break 78 | end 79 | end 80 | return s 81 | end 82 | 83 | local function escaped(view, s) 84 | if s > 1 and sub(view, s - 1, s - 1) == "\\" then 85 | if s > 2 and sub(view, s - 2, s - 2) == "\\" then 86 | return false, 1 87 | else 88 | return true, 1 89 | end 90 | end 91 | return false, 0 92 | end 93 | 94 | local function readfile(path) 95 | local file = open(path, "rb") 96 | if not file then return nil end 97 | local content = file:read "*a" 98 | file:close() 99 | return content 100 | end 101 | 102 | local function loadlua(path) 103 | return readfile(path) or path 104 | end 105 | 106 | local function loadngx(path) 107 | local vars = VAR_PHASES[phase()] 108 | local file, location = path, vars and var.template_location 109 | if sub(file, 1) == "/" then file = sub(file, 2) end 110 | if location and location ~= "" then 111 | if sub(location, -1) == "/" then location = sub(location, 1, -2) end 112 | local res = capture(concat{ location, '/', file}) 113 | if res.status == 200 then return res.body end 114 | end 115 | local root = vars and (var.template_root or var.document_root) or prefix 116 | if sub(root, -1) == "/" then root = sub(root, 1, -2) end 117 | return readfile(concat{ root, "/", file }) or path 118 | end 119 | 120 | do 121 | if ngx then 122 | VAR_PHASES = { 123 | set = true, 124 | rewrite = true, 125 | access = true, 126 | content = true, 127 | header_filter = true, 128 | body_filter = true, 129 | log = true 130 | } 131 | template.print = ngx.print or write 132 | template.load = loadngx 133 | prefix, var, capture, null, phase = ngx.config.prefix(), ngx.var, ngx.location.capture, ngx.null, ngx.get_phase 134 | if VAR_PHASES[phase()] then 135 | caching = enabled(var.template_cache) 136 | end 137 | else 138 | template.print = write 139 | template.load = loadlua 140 | end 141 | if _VERSION == "Lua 5.1" then 142 | local context = { __index = function(t, k) 143 | return t.context[k] or t.template[k] or _G[k] 144 | end } 145 | if jit then 146 | loadchunk = function(view) 147 | return assert(load(view, nil, "tb", setmetatable({ template = template }, context))) 148 | end 149 | else 150 | loadchunk = function(view) 151 | local func = assert(loadstring(view)) 152 | setfenv(func, setmetatable({ template = template }, context)) 153 | return func 154 | end 155 | end 156 | else 157 | local context = { __index = function(t, k) 158 | return t.context[k] or t.template[k] or _ENV[k] 159 | end } 160 | loadchunk = function(view) 161 | return assert(load(view, nil, "tb", setmetatable({ template = template }, context))) 162 | end 163 | end 164 | end 165 | 166 | function template.caching(enable) 167 | if enable ~= nil then caching = enable == true end 168 | return caching 169 | end 170 | 171 | function template.output(s) 172 | if s == nil or s == null then return "" end 173 | if type(s) == "function" then return template.output(s()) end 174 | return tostring(s) 175 | end 176 | 177 | function template.escape(s, c) 178 | if type(s) == "string" then 179 | if c then return gsub(s, "[}{\">/<'&]", CODE_ENTITIES) end 180 | return gsub(s, "[\">/<'&]", HTML_ENTITIES) 181 | end 182 | return template.output(s) 183 | end 184 | 185 | function template.new(view, layout) 186 | assert(view, "view was not provided for template.new(view, layout).") 187 | local render, compile = template.render, template.compile 188 | if layout then 189 | if type(layout) == "table" then 190 | return setmetatable({ render = function(self, context) 191 | local context = context or self 192 | context.blocks = context.blocks or {} 193 | context.view = compile(view)(context) 194 | layout.blocks = context.blocks or {} 195 | layout.view = context.view or "" 196 | return layout:render() 197 | end }, { __tostring = function(self) 198 | local context = self 199 | context.blocks = context.blocks or {} 200 | context.view = compile(view)(context) 201 | layout.blocks = context.blocks or {} 202 | layout.view = context.view 203 | return tostring(layout) 204 | end }) 205 | else 206 | return setmetatable({ render = function(self, context) 207 | local context = context or self 208 | context.blocks = context.blocks or {} 209 | context.view = compile(view)(context) 210 | return render(layout, context) 211 | end }, { __tostring = function(self) 212 | local context = self 213 | context.blocks = context.blocks or {} 214 | context.view = compile(view)(context) 215 | return compile(layout)(context) 216 | end }) 217 | end 218 | end 219 | return setmetatable({ render = function(self, context) 220 | return render(view, context or self) 221 | end }, { __tostring = function(self) 222 | return compile(view)(self) 223 | end }) 224 | end 225 | 226 | function template.precompile(view, path, strip) 227 | local chunk = dump(template.compile(view), strip ~= false) 228 | if path then 229 | local file = open(path, "wb") 230 | file:write(chunk) 231 | file:close() 232 | end 233 | return chunk 234 | end 235 | 236 | function template.compile(view, key, plain) 237 | assert(view, "view was not provided for template.compile(view, key, plain).") 238 | if key == "no-cache" then 239 | return loadchunk(template.parse(view, plain)), false 240 | end 241 | key = key or view 242 | local cache = template.cache 243 | if cache[key] then return cache[key], true end 244 | local func = loadchunk(template.parse(view, plain)) 245 | if caching then cache[key] = func end 246 | return func, false 247 | end 248 | 249 | function template.parse(view, plain) 250 | assert(view, "view was not provided for template.parse(view, plain).") 251 | if not plain then 252 | view = template.load(view) 253 | if byte(sub(view, 1, 1)) == 27 then return view end 254 | end 255 | local j = 2 256 | local c = {[[ 257 | context=... or {} 258 | local function include(v, c) return template.compile(v)(c or context) end 259 | local ___,blocks,layout={},blocks or {} 260 | ]] } 261 | local i, s = 1, find(view, "{", 1, true) 262 | while s do 263 | local t, p = sub(view, s + 1, s + 1), s + 2 264 | if t == "{" then 265 | local e = find(view, "}}", p, true) 266 | if e then 267 | local z, w = escaped(view, s) 268 | if i < s - w then 269 | c[j] = "___[#___+1]=[=[\n" 270 | c[j+1] = sub(view, i, s - 1 - w) 271 | c[j+2] = "]=]\n" 272 | j=j+3 273 | end 274 | if z then 275 | i = s 276 | else 277 | c[j] = "___[#___+1]=template.escape(" 278 | c[j+1] = trim(sub(view, p, e - 1)) 279 | c[j+2] = ")\n" 280 | j=j+3 281 | s, i = e + 1, e + 2 282 | end 283 | end 284 | elseif t == "*" then 285 | local e = find(view, "*}", p, true) 286 | if e then 287 | local z, w = escaped(view, s) 288 | if i < s - w then 289 | c[j] = "___[#___+1]=[=[\n" 290 | c[j+1] = sub(view, i, s - 1 - w) 291 | c[j+2] = "]=]\n" 292 | j=j+3 293 | end 294 | if z then 295 | i = s 296 | else 297 | c[j] = "___[#___+1]=template.output(" 298 | c[j+1] = trim(sub(view, p, e - 1)) 299 | c[j+2] = ")\n" 300 | j=j+3 301 | s, i = e + 1, e + 2 302 | end 303 | end 304 | elseif t == "%" then 305 | local e = find(view, "%}", p, true) 306 | if e then 307 | local z, w = escaped(view, s) 308 | if z then 309 | if i < s - w then 310 | c[j] = "___[#___+1]=[=[\n" 311 | c[j+1] = sub(view, i, s - 1 - w) 312 | c[j+2] = "]=]\n" 313 | j=j+3 314 | end 315 | i = s 316 | else 317 | local n = e + 2 318 | if sub(view, n, n) == "\n" then 319 | n = n + 1 320 | end 321 | local r = rpos(view, s - 1) 322 | if i <= r then 323 | c[j] = "___[#___+1]=[=[\n" 324 | c[j+1] = sub(view, i, r) 325 | c[j+2] = "]=]\n" 326 | j=j+3 327 | end 328 | c[j] = trim(sub(view, p, e - 1)) 329 | c[j+1] = "\n" 330 | j=j+2 331 | s, i = n - 1, n 332 | end 333 | end 334 | elseif t == "(" then 335 | local e = find(view, ")}", p, true) 336 | if e then 337 | local z, w = escaped(view, s) 338 | if i < s - w then 339 | c[j] = "___[#___+1]=[=[\n" 340 | c[j+1] = sub(view, i, s - 1 - w) 341 | c[j+2] = "]=]\n" 342 | j=j+3 343 | end 344 | if z then 345 | i = s 346 | else 347 | local f = sub(view, p, e - 1) 348 | local x = find(f, ",", 2, true) 349 | if x then 350 | c[j] = "___[#___+1]=include([=[" 351 | c[j+1] = trim(sub(f, 1, x - 1)) 352 | c[j+2] = "]=]," 353 | c[j+3] = trim(sub(f, x + 1)) 354 | c[j+4] = ")\n" 355 | j=j+5 356 | else 357 | c[j] = "___[#___+1]=include([=[" 358 | c[j+1] = trim(f) 359 | c[j+2] = "]=])\n" 360 | j=j+3 361 | end 362 | s, i = e + 1, e + 2 363 | end 364 | end 365 | elseif t == "[" then 366 | local e = find(view, "]}", p, true) 367 | if e then 368 | local z, w = escaped(view, s) 369 | if i < s - w then 370 | c[j] = "___[#___+1]=[=[\n" 371 | c[j+1] = sub(view, i, s - 1 - w) 372 | c[j+2] = "]=]\n" 373 | j=j+3 374 | end 375 | if z then 376 | i = s 377 | else 378 | c[j] = "___[#___+1]=include(" 379 | c[j+1] = trim(sub(view, p, e - 1)) 380 | c[j+2] = ")\n" 381 | j=j+3 382 | s, i = e + 1, e + 2 383 | end 384 | end 385 | elseif t == "-" then 386 | local e = find(view, "-}", p, true) 387 | if e then 388 | local x, y = find(view, sub(view, s, e + 1), e + 2, true) 389 | if x then 390 | local z, w = escaped(view, s) 391 | if z then 392 | if i < s - w then 393 | c[j] = "___[#___+1]=[=[\n" 394 | c[j+1] = sub(view, i, s - 1 - w) 395 | c[j+2] = "]=]\n" 396 | j=j+3 397 | end 398 | i = s 399 | else 400 | y = y + 1 401 | x = x - 1 402 | if sub(view, y, y) == "\n" then 403 | y = y + 1 404 | end 405 | local b = trim(sub(view, p, e - 1)) 406 | if b == "verbatim" or b == "raw" then 407 | if i < s - w then 408 | c[j] = "___[#___+1]=[=[\n" 409 | c[j+1] = sub(view, i, s - 1 - w) 410 | c[j+2] = "]=]\n" 411 | j=j+3 412 | end 413 | c[j] = "___[#___+1]=[=[" 414 | c[j+1] = sub(view, e + 2, x) 415 | c[j+2] = "]=]\n" 416 | j=j+3 417 | else 418 | if sub(view, x, x) == "\n" then 419 | x = x - 1 420 | end 421 | local r = rpos(view, s - 1) 422 | if i <= r then 423 | c[j] = "___[#___+1]=[=[\n" 424 | c[j+1] = sub(view, i, r) 425 | c[j+2] = "]=]\n" 426 | j=j+3 427 | end 428 | c[j] = 'blocks["' 429 | c[j+1] = b 430 | c[j+2] = '"]=include[=[' 431 | c[j+3] = sub(view, e + 2, x) 432 | c[j+4] = "]=]\n" 433 | j=j+5 434 | end 435 | s, i = y - 1, y 436 | end 437 | end 438 | end 439 | elseif t == "#" then 440 | local e = find(view, "#}", p, true) 441 | if e then 442 | local z, w = escaped(view, s) 443 | if i < s - w then 444 | c[j] = "___[#___+1]=[=[\n" 445 | c[j+1] = sub(view, i, s - 1 - w) 446 | c[j+2] = "]=]\n" 447 | j=j+3 448 | end 449 | if z then 450 | i = s 451 | else 452 | e = e + 2 453 | if sub(view, e, e) == "\n" then 454 | e = e + 1 455 | end 456 | s, i = e - 1, e 457 | end 458 | end 459 | end 460 | s = find(view, "{", s + 1, true) 461 | end 462 | s = sub(view, i) 463 | if s and s ~= "" then 464 | c[j] = "___[#___+1]=[=[\n" 465 | c[j+1] = s 466 | c[j+2] = "]=]\n" 467 | j=j+3 468 | end 469 | c[j] = "return layout and include(layout,setmetatable({view=table.concat(___),blocks=blocks},{__index=context})) or table.concat(___)" 470 | return concat(c) 471 | end 472 | 473 | function template.render(view, context, key, plain) 474 | assert(view, "view was not provided for template.render(view, context, key, plain).") 475 | return template.print(template.compile(view, key, plain)(context)) 476 | end 477 | 478 | return template 479 | -------------------------------------------------------------------------------- /README-cn.md: -------------------------------------------------------------------------------- 1 | # lua-resty-yii 2 | 3 | 一个基于OpenResty的仿Yii的web框架,通过本框架能够极大降低openresty的开发入门门槛。 [English](README.md) 4 | 5 | # 系统要求 6 | 7 | 安装OpenResty 1.0以上版本 https://openresty.org/en/download.html 8 | 9 | # 快速开始 10 | 11 | **Linux:修改runtime/start.sh中/usr/local/openresty/bin/openresty路径为您所安装的openresty路径** 12 | 13 | 开始,bash中cd到当前目录,执行:runtime/start.sh 14 | 停止,bash中cd到当前目录,执行:runtime/stop.sh 15 | 16 | **Windows:修改runtime/start.bat中D:\openresty-1.11.2.2-win32\路径为您所安装的openresty路径** 17 | 18 | 开始,双击:runtime/win-start.bat 19 | 停止,双击:runtime/win-stop.bat 20 | 21 | # 开发说明 22 | ## 应用结构 23 | 目录及文件: 24 | |____index.lua #入口文件 25 | |____lua-releng #检查代码质量/是否使用了全局变量 26 | |____config #配置文件目录 27 | | |____db.lua #数据库配置 28 | | |____lang.lua #语言包配置 29 | | |____memcache.lua #memcache配置 30 | | |____session.lua #session配置 31 | | |____web.lua #网站基本信息配置 32 | |____controllers #controller目录 33 | | |____site.lua #前端页面 34 | |____models #models目录 35 | | |____LoginForm.lua #登录逻辑处理类 36 | | |____Userinfo.lua #用户信息管理类 37 | |____runtime #Openresty运行目录 38 | | |____client_body_temp #post上传的临时保存目录 39 | | |____fastcgi_temp #fastcig临时目录 40 | | |____logs #站点日志目录 41 | | | |____access.log #请求日志 42 | | | |____error.log #错误日志 43 | | | |____nginx.pid #nginx进程文件 44 | | |____nginx.conf #站点nginx配置文件 45 | | |____proxy_temp #proxy_temp 46 | | |____scgi_temp #scgi_temp 47 | | |____start.sh #站点启动程序 48 | | |____stop.sh #站点停止程序 49 | | |____uwsgi_temp #uwsgi_temp 50 | |____vendor #框架及第三方类 51 | | |____ActiveRecord.lua #数据库处理基类 52 | | |____Application.lua #请求处理类 53 | | |____Controller.lua #Controller基类 54 | | |____Files.lua #上传文件接收类 55 | | |____Memcache.lua #Memcache操作类 56 | | |____Model.lua #Model基类 57 | | |____Mysql.lua #Mysql操作类 58 | | |____Pager.lua #分页类 59 | | |____Query.lua #查询构建器 60 | | |____Request.lua #请求信息工具类 61 | | |____resty #第三方resty工具 62 | | | |____http.lua #http请求工具 63 | | | |____template.lua #lua模版工具类 64 | | |____Session.lua #Session操作类 65 | | |____User.lua #用户信息操作类 66 | | |____Util.lua #基本工具类 67 | |____views #页面模版目录 68 | | |____layout #页面框架目录 69 | | | |____main.lua.html #基本站点框架 70 | | |____site #站点页面目录 71 | | | |____error.lua.html #错误信息页 72 | | | |____guide.lua.html #开发说明页 73 | | | |____index.lua.html #首页 74 | | | |____login.lua.html #登录页 75 | |____web #静态资源目录 76 | | |____css #样式 77 | | | |____bootstrap-theme.css #bootstrap 78 | | | |____bootstrap-theme.min.css #bootstrap 79 | | | |____bootstrap.css #bootstrap 80 | | | |____bootstrap.min.css #bootstrap 81 | | | |____site.css #站点样式 82 | | |____js #javascript 83 | | | |____bootstrap.js #bootstrap 84 | | | |____bootstrap.min.js #bootstrap 85 | | | |____jquery.js #jquery 86 | | | |____jquery.min.js #jquery 87 | | | |____jquery.slim.js #jquery 88 | | | |____jquery.slim.min.js #jquery 89 | | |____favicon.ico #图标 90 | | |____robots.txt #robots.txt 91 | ## 运行机制概述 92 | 每一次应用开始处理 HTTP 请求时,它都会进行一个近似的流程。 93 | 94 | - **用户提交指向 入口脚本 index.lua 的请求** 95 | - **入口脚本会创建一个 应用(Application) 实例用于处理该请求,并加载配置。** 96 | - **应用会通过 request(请求) 应用组件解析被请求的路由。** 97 | - **应用创建一个 controller(控制器) 实例具体处理请求。** 98 | - **执行controller中的before()方法进行请求过滤。** 99 | - **如果执行before()返回true,则继续执行 action(动作),否则终止。** 100 | - **动作会加载一个数据模型,一般是从数据库中加载。** 101 | - **动作会渲染一个 View(视图),并为其提供所需的数据模型。** 102 | - **渲染得到的结果会返回给 response(响应) 应用组件。** 103 | - **响应组件会把渲染结果发回给用户的浏览器。** 104 | 105 | ## 调试模式: 106 | - **访问 /lua/index 会使用lua_code_cache off的模式访问站点** 107 | - **访问 /lua/{filepath} 可调试对应的lua脚本** 108 | 109 | ## 应用(Application) 110 | 每接收到一个 HTTP 请求,入口脚本 index.lua 会创建一个 应用(Application) 实例用于处理该请求 111 | Application实例创建后,会保存覆盖全局变量ngx.ctx(此全局变量生命周期对应每个请求) 112 | ``` lua 113 | ngx.ctx = require("vendor.Application"):new() 114 | ``` 115 | Application在创建时,会加载网站的基本配置及基本工具作为属性,如: 116 | - **ngx.ctx.web 相当于 require("config.web"), --站点配置信息表** 117 | - **ngx.ctx.lang 相当于 require("config.lang"), --语言包配置信息** 118 | - **ngx.ctx.session 相当于 require("config.session"), --站点Session类** 119 | - **ngx.ctx.user 相当于 require("vendor.User"), --站点用户工具类** 120 | - **ngx.ctx.request 相当于 require("vendor.Request"), --站点请求处理类** 121 | 122 | 在执行Application的new()时,会创建一个继承于Application的Controller实例, 123 | 因此ngx.ctx即是一个Application也是一个Controller实例,并且controller中也可以通过 124 | self.web,self.lang,self.session.self.user.self.request直接引用以上配置及工具 125 | 126 | **注意:在使用时,尽量少使用全局变量ngx.ctx,而应该通过函数传值的方式进行传递,能获得更快的运行速度** 127 | 128 | ## 控制器(Controllers): 129 | ``` lua 130 | local _M = require("vendor.Controller"):new{_VERSION='0.01'} --生成Controller新实例 131 | 132 | function _M:indexAction() --动作方法名必须以name+Action组成 133 | if not self.user.isLogin then return self:loginAction() end --使用用户信息类判断用户是否登录 134 | 135 | local Product = require('models.Product') --使用Product数据表操作类 136 | local query = Product.find().where{'in','product_id',self.user.resource} --fin()方法生成新的查询器 137 | 138 | local page = Pager{ --使用分页类 139 | pageSize = 8, --设置每页显示最多8条信息 140 | totalCount = query.count(), --使用查询器查询符合条件的数据总条数 141 | } 142 | 143 | local list = query.orderBy('create_time desc').page(page).all() --使用查询器查询分页数据集 144 | 145 | return self:render('index',{ 146 | page = page, 147 | list = list, 148 | }) 149 | end 150 | 151 | retrun _M 152 | ``` 153 | **如果保存为controllers/filename.lua,则访问?act=filename.index时,会执行上面对应的indexAction()方法** 154 | 155 | ##模型(Models): 156 | 模型是 MVC 模式中的一部分, 是代表业务数据、规则和逻辑的对象。 157 | 可通过继承 "vendor.Model" 或它的子类定义模型类, 基类"vendor.Model"支持许多实用的特性,如: 158 | ``` lua 159 | local _M = require("vendor.Model"):new{_version='0.0.1'} --生成Model新实例 160 | 161 | local Userinfo = require('models.Userinfo') --在Model里使用其他model 162 | 163 | function _M.rules() --方法rules添加数据校验规则 164 | return { 165 | {{'username','password','sacode'}, 'trim'}, --通过trim自动过滤输入 166 | {'username', 'required',message='请填写登录账号'}, --通过required规则设置必须填写 167 | --{'username', 'email'}, --如果需要用户名必须为email时设置 168 | {'password', 'required',message='请填写登录密码'}, 169 | --使用自定义方法校验参数 170 | {'password','checkPass'} --使用自定义checkPass方法进行校验 171 | } 172 | end 173 | 174 | function _M:checkPass(key) 175 | if self:hasErrors() then return end 176 | 177 | local user = Userinfo.getUserByName(self.username) 178 | if not user then 179 | self:addError('username','账号不存在') 180 | elseif user.password ~= self.password then 181 | self:addError('password','密码错误') 182 | else 183 | self.userinfo = user 184 | end 185 | end 186 | 187 | function _M:login(user) 188 | if not self:validate() then return false end 189 | 190 | return user.login(self.userinfo, rememberMe and 3600*24*30 or 0) 191 | end 192 | 193 | return _M 194 | ``` 195 | 196 | 在Controller里使用Model: 197 | ``` lua 198 | --因为Model包含有数据,一定要调用new方法生成新实例,避免出现数据缓存问题 199 | local model = require('models.LoginForm'):new() 200 | 201 | --通过model的load()方法,可自动装载数据 202 | if model:load(self.request.post()) and model:login(self.user) then 203 | return self:goHome() 204 | end 205 | ``` 206 | 207 | ## 视图(Views): 208 | 视图基于[lua-resty-template](https://github.com/bungle/lua-resty-template)实现 209 | 210 | 在视图中必须使用以下标签: 211 | - **{{expression}}, 输出expression表达式,并用经过html格式化** 212 | - **{*expression*}, 原样输出expression表达式的结果** 213 | - **{% lua code %}, 执行Lua代码** 214 | - **{# comments #}, 所有{#和#}之间的内容都被认为是注释掉的(即不输出或执行)** 215 | 216 | 与lua-resty-template的普通用法不同,所有视图文件,以lua文件的形式保存在views子目录下,文件名为*.lua.html 217 | controller中的render方法渲染试图时,会以require的形式去获取视图内容 218 | 219 | views/layout 目录存放框架视图,渲染视图时默认使用views/layout/main.lua.html, 220 | controller中可通过layout属性设置使用不同的框架视图,如: 221 | ``` lua 222 | local _M = require("vendor.Controller"):new{layout = 'manage'} 223 | ``` 224 | views下的其他子目录分别为不同功能模块对应的内容视图,所有页头,页尾,菜单等内容应在框架视图中实现 225 | 内容视图中不推荐使用{(template)}的形式来包含其他视图,因与默认渲染方式不同,会导致找不到文件等错误 226 | 227 | controller在渲染视图时,会将所有application及controller的数据传递给视图,因此在视图中可以直接使用这些数据 228 | 如:{{lang.siteName}}可输出语言包中配置的网站名称 229 | 230 | 视图中可以通过设置context属性的方式用于内容视图跟框架视图之间传值,如设置: 231 | {% context.title = '开发说明' %} 232 | 则可以在框架视图中{{title}}输出显示 233 | 234 | ## 请求处理 235 | ### 获取请求参数 236 | ``` lua 237 | local request = require("vendor.Request") 238 | --在Controller方法里可以直接通过self.request调用 239 | 240 | local get = request.get() 241 | --等价于php: $get = $_GET; 242 | 243 | local id = request.get('id'); 244 | --等价于php: $id = isset($_GET['id']) ? $_GET['id'] : null; 245 | 246 | local id = request.get('id', 1) 247 | --等价于php: $id = isset($_GET['id']) ? $_GET['id'] : 1; 248 | 249 | local post = request.post() 250 | --等价于php: $post = $_POST; 251 | 252 | local name = request.post('name') 253 | --等价于php: $name = isset($_POST['name']) ? $_POST['name'] : null; 254 | 255 | local name = request.post('name', '') 256 | --等价于php: $name = isset($_POST['name']) ? $_POST['name'] : ''; 257 | 258 | local cookie = request.cookie() 259 | --等价于php: $cookie = $_COOKIE; 260 | 261 | local sso = request.cookie('sso') 262 | --等价于php: $sso = isset($_COOKIE['sso']) ? $_COOKIE['sso'] : null; 263 | 264 | local sso = request.cookie('sso', '') 265 | --等价于php: $sso = isset($_COOKIE['sso']) ? $_COOKIE['sso'] : ''; 266 | 267 | --判断是否有上传文件 268 | ngx.say(request.isPostFile()) 269 | --注意跟php不同的地方,如果有上传文件时,普通参数是无法通过request.post()方法获取到的 270 | --建议上传文件时,普通参数通过GET传递 271 | 272 | --接收上传文件 273 | local file = require('vendor.Files') 274 | local savename = path..filename 275 | 276 | local ok,err = file.receive('upfile',savename) --接收name为upfile的上传文件 277 | if not ok then 278 | return {retcode=1,retmsg='接收文件失败,请重试:'..err} 279 | end 280 | ``` 281 | ### 设置cookie: 282 | ``` lua 283 | local util = require "vendor.Util" 284 | util.set_cookie('abc','123') 285 | util.set_cookie('def','456',3600) 286 | util.set_cookie(name,value,expire,path,domain,secure,httponly) 287 | ``` 288 | ### Session操作: 289 | ``` lua 290 | ocal session = require "vendor.Session" 291 | --在Controller方法里可以直接通过self.session调用 292 | --session.start() --启用session,会去设置一个_sessionid的cookie,此操作可以省略 293 | --session.exptime = 1800 --修改session失效时间时,默认30分钟 294 | --session.cache = ngx_shared['session_cache'] --默认使用ngx的缓存,对应runtime/nginx.conf里的lua_shared_dict配置项 295 | --session.cache = require("vendor.Memcache"):new{host="127.0.0.1",port="11211"} --使用memcached缓存打开此配置 296 | 297 | local sessions = session.get() 298 | --等价于php: $sessions = $_SESSION; 299 | 300 | local abc = session.get('abc') 301 | --等价于:local abc = session.abc 302 | --等价于:local abc = session['abc'] 303 | --等价于php: $abc = isset($_SESSION['abc']) ? $_SESSION['abc'] : null; 304 | 305 | local abc = session.get('abc', '') 306 | --等价于php: $abc = isset($_SESSION['abc']) ? $_SESSION['abc'] : ''; 307 | 308 | --设置session值 309 | session.set('abc','abc-value') 310 | --等价于:session.abc = 'abc-value' 311 | --等价于:session['abc'] = 'abc-value' 312 | ``` 313 | 314 | ## 数据库操作(Working with Databases: 315 | 316 | ### 数据库访问 (DAO) 317 | ```lua 318 | local db = require('vendor.Mysql'):new{ 319 | host = "127.0.0.1", 320 | port = 3306, 321 | database = "mytest", 322 | user = "root", 323 | password = "", 324 | charset = "utf8", --建议配置Mysql的默认连接字符集为utf8,可去掉此配置项,此项配置会增加一次'SET NAMES utf8'的操作 325 | } 326 | 327 | --或者直接通过配置文件获得: 328 | local db = require('config.db') 329 | 330 | --执行sql: 331 | local rows = db:query('select * from mytable') 332 | ngx.say(#rows) 333 | ``` 334 | ### 使用查询生成器 (Query Builder) 335 | vendor.Query 封装了 vendor.Mysql,并提供快捷构建 安全 查询语句的方法,例如: 336 | ```lua 337 | local db = require('config.db') 338 | local query = require('vendor.Query')() 339 | local rows = query.use(db).from('products').where({product_id=123}).all() 340 | --相当于执行local rows = db:query("select * from products where product_id='123'") 341 | ngx.say(query.get('sql')) --能获取到构造出的sql语句 342 | 343 | --query.use()用于指定链接的数据库 344 | query.use(require('config.db')) 345 | 346 | --query.select()用于指定要查询的字段,不指定默认为select * 347 | query.select({'id', 'email'}) 348 | --等同于: 349 | query.select('id, email') 350 | 351 | --query.from()用于指定要查询的表格 SELECT * FROM `user` 352 | query.from('user') 353 | ``` 354 | **query.where()用于定义 SQL 语句当中的 WHERE 子句。 你可以使用如下三种格式来定义 WHERE 条件:** 355 | 356 | - **字符串格式**,例如:'status=1' , 这个方法不会自动加引号或者转义。 357 | 358 | - **哈希格式**,例如: {status=1,type=2} ,这个方法将正确地为字段名加引号以及为取值范围转义 359 | 360 | - **操作符格式**,例如:{'in', {'2012','2011'}} 361 | 操作符格式允许你指定类程序风格的任意条件语句,如下所示: 362 | {操作符, 操作数1, 操作数2, ...} 363 | 其中每个操作数可以是字符串格式、哈希格式或者嵌套的操作符格式, 而操作符可以是如下列表中的一个: 364 | 365 | 操作符 | 用法 366 | ---------- | -------------------------------------------------------------- 367 | **and** | 操作数会被 AND 关键字串联起来。例如,{'and', 'id=1', 'id=2'} 将会生成 id=1 AND id=2。如果操作数是一个数组,它也会按上述规则转换成字符串。例如,{'and', 'type=1', {'or', 'id=1', 'id=2'}} 将会生成 type=1 AND (id=1 OR id=2)。 这个方法不会自动加引号或者转义。 368 | **or** | 用法和 and 操作符类似。 369 | **between** | 第一个操作数为字段名称,第二个和第三个操作数代表的是这个字段 的取值范围。例如,{'between', 'id', 1, 10} 将会生成 id BETWEEN 1 AND 10。 370 | **not between** | 用法跟between类似 371 | **in** | 第一个操作数应为字段名称,第二个操作符既是一个数组。 例如, {'in', 'id', {1, 2, 3}} 将生成 `id` IN ('1', '2', '3') 。该方法将正确地为字段名加引号以及为取值范围转义 372 | **not in** | 用法和 in 操作符类似。 373 | 374 | **query.orderBy() 方法是用来指定 SQL 语句当中的 ORDER BY 子句的。** 375 | 例如,要实现 ... ORDER BY create_time desc 可以: 376 | ``` lua 377 | query.orderBy('create_time desc') 378 | --等价于:query.orderBy{create_time=desc} 379 | ``` 380 | 381 | **query.groupBy() 方法是用来指定 SQL 语句当中的 GROUP BY 片断的。** 382 | 例如,要实现 ... GROUP BY `id`, `status` 可以: 383 | ``` lua 384 | query.groupBy{'id', 'status'} 385 | ``` 386 | 387 | **query.limit() 和 query.offset() 是用来指定 SQL 语句当中 的 LIMIT 和 OFFSET 子句的。** 388 | 例如,要实现 ... LIMIT 10 OFFSET 20 等价于mysql的limit 20,10 可以: 389 | ``` lua 390 | query.limit(10).offset(20) 391 | ``` 392 | 如果你指定了一个无效的 limit 或者 offset(例如,一个负数),那么它将会被忽略掉。 393 | 394 | 395 | **query.page() 能通过传递"vendor.Pager"对象,设置 LIMIT 和 OFFSET 达到分页查询的目的:** 396 | ``` lua 397 | local page = require("vendor.Pager"){ 398 | pageSize = 8, 399 | totalCount = 10, 400 | } 401 | query.page(page) --等价于:query.limit(page.limit).offset(page.offset) 402 | ``` 403 | ####查询方法 404 | vendor.Query 提供了一整套的用于不同查询目的的方法。 405 | * query.all(): 将返回一个由行组成的数组。 406 | * query.one(): 返回结果集的第一行。 407 | * query.count(): 返回 COUNT 查询的结果。 408 | 409 | 例如: 410 | ``` lua 411 | local db = require('config.db') 412 | local query = require('vendor.Query'){}.use(db).from('products').where{product_id=123} 413 | local rows = query.all() 414 | local row = query.one() 415 | local count = query.count() 416 | ``` 417 | 418 | 其他方法: 419 | * query.insert(): 插入数据。 420 | * query.update(): 更新数据。 421 | * query.delete(): 删除数据。 422 | * query.save(): 新数据调用等价于query.insert(),查询结果集调用等价于query.update()。 423 | 424 | 例如: 425 | ``` lua 426 | local db = require('config.db') 427 | local data = {product_id=123,product_name='name123'} 428 | local query = require('vendor.Query')(data).use(db).from('products') 429 | 430 | --插入: 431 | local row = query.insert() 432 | --要获取新插入的数据的自增长id,请使用: 433 | local id = row.get('insert_id') --等价于query.get('insert_id') 434 | 435 | --更新: 436 | row.product_name = 'name456' 437 | row.update() --或者query.update(row) 438 | 439 | --删除: 440 | row.delete() --或者query.update(row) 441 | ``` 442 | 443 | ###使用活动记录 (Active Record) 444 | vendor.ActiveRecord 进一步封装了查询生成器vendor.Query 445 | 同时vendor.ActiveRecord继承于vendor.Model,能够使用模型对象的load(),rules(),hasErrors(),validate()等方法 446 | 447 | 使用示例: 448 | ``` lua 449 | local Product = require('vendor.ActiveRecord'){ 450 | --db = require('config.db'), --可选属性,指定使用的数据库连接,未设置时默认使用'config.db' 451 | tableName = function() --必须实现tableName方法,返回要操作的数据表名称 452 | return 'products' 453 | end 454 | } 455 | 456 | --插入数据: 457 | local product = Product.new{ 458 | product_id = 123, 459 | product_name = 'name123', 460 | } --返回的对象支持使用vendor.Query方法 461 | product.save() --等价于product.insert() 462 | 463 | --快速查找一行 464 | local row = Product.findOne{product_id = 123} --返回的对象支持使用vendor.Query方法 465 | 466 | --快速查找多行 467 | local rows = Product.findAll{user='creater'} --返回的对象支持使用vendor.Query方法 468 | 469 | --复杂查询 470 | local query = Product.find().where{user='creater'} --返回的对象支持使用vendor.Query方法 471 | local page = Pager{ 472 | pageSize = 8, 473 | totalCount = query.count(), 474 | } 475 | local list = query.orderBy('create_time desc').page(page).all() 476 | 477 | --同样可以执行以下操作 478 | query.insert() --插入数据。 479 | query.update() --更新数据。 480 | query.delete() --删除数据。 481 | ``` 482 | 建议每个数据表格在models目录下建立一个继承于'vendor.ActiveRecord'的操作类 483 | 484 | ###数据库安全问题 485 | 使用vendor.ActiveRecord 或 vendor.Query 自动生成查询语句 486 | 在构造sql语句的过程中,如果传递的参数是表格,构造器会进行转义操作,防止sql注入 487 | 488 | 但如果传递的是字符串,则不会进行转义操作,建议尽量少用,并能确保sql安全 489 | 可以通过 query.get('sql') 获取执行的sql语句 490 | 491 | 如果要进行转义,可使用util.mescape()方法 492 | ```lua 493 | local util = require("vendor.Util") 494 | value = util.mescape(value) 495 | ``` 496 | 497 | lua-resty-yii is available under the MIT license. See the [LICENSE file][1] 498 | for more information. 499 | 500 | [1]: ./LICENSE.txt 501 | 502 | 503 | 504 | -------------------------------------------------------------------------------- /web/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lua-resty-yii 2 | 3 | A network framework based on OpenResty Imitation Yii, through this framework can greatly reduce the entry threshold of openresty development. [中文](README-cn.md) 4 | 5 | #System Requirements 6 | 7 | Install OpenResty 1.0+ https://openresty.org/en/download.html 8 | 9 | # Quick start 10 | 11 | **Linux OS: Modify the /usr/local/openresty/bin/openresty path in your runtime/start.sh to your openresty path** 12 | 13 | At the beginning, bash CD to the current directory, execute: runtime/start.sh 14 | Stop, celebrate the CD to the current directory, execute: runtime/stop.sh 15 | 16 | **Windows: Modify runtime/start.bat in d:\openresty-1.11.2.2--Win32\ path for your installed openresty path** 17 | 18 | Start, double-click: runtime/win-start.bat 19 | Stop, double-click: runtime/win-stop.bat 20 | 21 | # Development instructions 22 | 23 | ## application structure 24 | 25 | Directory and documents: 26 | | ____ index.lua # entry file 27 | | ____ lua-releng # Check code quality / whether global variables are used 28 | | ____ config # configuration file directory 29 | | | ____ db.lua # database configuration 30 | | | ____ lang.lua # language pack configuration 31 | | | ____ memcache.lua # memcache configuration 32 | | | ____ session.lua #session configuration 33 | | ____ web.lua # Website basic information configuration 34 | | ____ Controller #controller directory 35 | | | ____ site.lua # front page 36 | | ____ Model #models directory 37 | | | ____ LoginForm.lua # Login logical processing class 38 | | ____ Userinfo.lua # User Information Management Class 39 | | ____ runtime # Openresty run directory 40 | | | ____ client_body_temp #post Upload temporary save directory 41 | | ____ fastcgi_temp #fastcig temporary directory 42 | | ____ Logs # The site log directory 43 | | | | ____ access.log # Request Log 44 | | | | ____ error.log # error log 45 | | | | ____ nginx.pid #nginx Process File 46 | | | ____ nginx.conf # Site nginx configuration file 47 | | | ____ proxy_temp #proxy_temp 48 | | | ____ scgi_temp #scgi_temp 49 | | ____ start.sh # Site Launcher 50 | | | ____ stop.sh # Stop the program 51 | | | ____ uwsgi_temp #uwsgi_temp 52 | | ____ vendor # framework and third-party classes 53 | | ____ ActiveRecord.lua # database processing base class 54 | | ____ Application.lua # Request processing class 55 | | | ____ Controller.lua #Controller base class 56 | | ____ Files.lua # Upload file receiving class 57 | | | ____ Memcache.lua #Memcache operation class 58 | | ____ Model.lua #Model base class 59 | | | ____ Mysql.lua #Mysql operation class 60 | | ____ Pager.lua # Paging classes 61 | | | ____ Query.lua # Query builder 62 | | ____ Request.lua # request information tool class 63 | | ____ resty # third-party resty tools 64 | | | | ____ http.lua #http Request Tool 65 | | | | ____ template.lua #lua Template Tools 66 | | | ____ Session.lua #Session action class 67 | | | ____ User.lua # user information action class 68 | | ____ Util.lua # Basic Tools 69 | | ____ views # page template directory 70 | | ____ layout # page frame directory 71 | | | | ____ main.lua.html # basic site framework 72 | | ____ site # site page directory 73 | | | | ____ error.lua.html # error message page 74 | | | | ____ guide.lua.html # Development Notes page 75 | | | | ____ index.lua.html # Home 76 | | | | ____ login.lua.html # Login page 77 | | ____ web # Static Resource Directory 78 | | | ____ css # style 79 | | | | ____ bootstrap-theme.css #bootstrap 80 | | | | ____ bootstrap-theme.min.css #bootstrap 81 | | | | ____ bootstrap.css #bootstrap 82 | | | | ____ bootstrap.min.css #bootstrap 83 | | | | ____ site.css # site style 84 | | | ____ js # javascript 85 | | | | ____ bootstrap.js #bootstrap 86 | | | | ____ bootstrap.min.js #bootstrap 87 | | | | ____ jquery.js #jquery 88 | | | | ____ jquery.min.js #jquery 89 | | | | ____ jquery.slim.js #jquery 90 | | | | ____ jquery.slim.min.js #jquery 91 | | | ____ favicon.ico # icon 92 | | | ____ robots.txt # robots.txt 93 | 94 | ## Overview of the operating mechanism 95 | 96 | Every time an application starts to process an HTTP request, it performs an approximate process. 97 | 98 | --**User submits request for entry script index.lua** 99 | --**The entry script creates an Application instance to handle the request and loads the configuration** 100 | --**The application parses the requested route through the request application component.** 101 | --**Application to create a Controller instance to handle the request.** 102 | --**Execute the before() method in the controller for request filtering.** 103 | --**Continue with action if before() returns true, otherwise terminate.** 104 | --**Action will load a data model, usually loaded from the database.** 105 | --**The action renders a View and provides it with the required data model.** 106 | --**Rendered results are returned to the response (response) application component.** 107 | --**The response component sends the result back to the user's browser.** 108 | 109 | ## Debug mode: 110 | 111 | --**Access / lua / index will use lua_code_cache off mode to access the site** 112 | --**Visit / lua / {filepath} Can debug the corresponding lua script** 113 | 114 | ## Application 115 | 116 | Each time an HTTP request is received, the entry script index.lua creates an instance of the application for processing the request 117 | After the application instance is created, the global variable ngx.ctx is overwritten (for each request corresponding to the life cycle of the global variable) 118 | 119 | ```lua 120 | ngx.ctx = require ("vendor.Application"): new() 121 | ``` 122 | 123 | When the application is created, it will load the basic configuration and basic tools of the website as attributes, such as: 124 | 125 | --**ngx.ctx.web is equivalent to require ("config.web"), --Site Configuration Information Table** 126 | --**ngx.ctx.lang is equivalent to require ("config.lang"), --language package configuration information** 127 | --**ngx.ctx.session is equivalent to require ("config.session"), --site session class** 128 | --**ngx.ctx.user is equivalent to require ("vendor.User"), --Site User Tools** 129 | --**ngx.ctx.request is equivalent to require ("vendor.Request"), --Site Request Processing** 130 | 131 | When executing a new() application, a controller instance is inherited from the application, 132 | So ngx.ctx is an application that is also a controller instance and is also available in the controller 133 | self.web, self.lang, self.session.self.user.self.request Direct reference to the above configuration and tools 134 | 135 | ** Note: When using, minimize the use of the global variable ngx.ctx, but should pass the value of the function pass, get faster speed ** 136 | 137 | ## Controller (Controller): 138 | 139 | ``` lua 140 | local _M = require("vendor.Controller"):new{_VERSION='0.01'} --Generate a new Controller instance 141 | 142 | function _M:indexAction() --The action method name must be name + Action 143 | if not self.user.isLogin then return self:loginAction() end --use the user information to determine whether the user login 144 | 145 | local Product = require('models.Product') --Uses the Product Datasheet operation class 146 | local query = Product.find().where{'in','product_id',self.user.resource} --The fin() method generates a new querier 147 | 148 | local page = Pager{ 149 | pageSize = 8, 150 | totalCount = query.count(), 151 | } 152 | 153 | local list = query.orderBy('create_time desc').page(page).all() 154 | 155 | return self:render('index',{ 156 | page = page, 157 | list = list, 158 | }) 159 | end 160 | 161 | retrun _M 162 | ``` 163 | 164 | ** If saved as controller / filename.lua then access? ACT = filename.index, it will perform the above corresponding indexAction() method ** 165 | 166 | ## model (model): 167 | The model is part of the MVC pattern and represents the business data, rules, and logic objects. 168 | The model class can be defined by inheriting "vendor.Model" or its subclasses. The base class "vendor.Model" supports many useful features such as: 169 | 170 | ```lua 171 | local _M = require ("vendor.Model"): new {_version = '0.0.1'} --Generate a new instance of Model 172 | 173 | Local Userinfo = require ('models.Userinfo') --Use other models in the Model 174 | 175 | Function _M.rules() --Method rules Add data validation rules 176 | Return { 177 | {{'Username', 'password', 'sacode'}, 'trim'}, --Filter input automatically through trim 178 | {'Username', 'required', message = 'Please fill in the login account'}, --Required by the required rules set 179 | --{'username', 'email'}, --if required username must be set for email time 180 | {'Password', 'required', message = 'Please fill in the login password'}, 181 | --Use custom methods to verify the parameters 182 | {'Password', 'checkPass'} --Validate using the custom checkPass method 183 | } 184 | End 185 | 186 | Function _M: checkPass (key) 187 | If self: hasErrors() then return to the end 188 | 189 | Local user = Userinfo.getUserByName (self.username) 190 | If not user then 191 | From: addError ('username', 'account does not exist') 192 | Elseif user.password ~ ​​= self.password Then 193 | From: addError ('password', 'wrong password') 194 | Other 195 | Self.userinfo = user 196 | End 197 | End 198 | 199 | Function _M: login (user) 200 | If not self: validate() then returns false 201 | The company is located in: 202 | Return user.login (self.userinfo, rememberMe and 3600 * 24 * 30 or 0) 203 | End 204 | 205 | return _M 206 | ``` 207 | 208 | Use in controller Model: 209 | 210 | ```lua 211 |  --Because the model contains data, be sure to call the new method to generate a new instance, to avoid data caching problems 212 | local model = require ('models.LoginForm'): new() 213 | 214 |  --Load data automatically through model load() method 215 | If model: load (self.request.post()) and model: login (self.user) then 216 | Back to myself: goHome() 217 | End 218 | ``` 219 | 220 | ## view (view): 221 | The view is based on the [LUA-resty template (https://github.com/bungle/lua-resty-template) 222 | 223 | The following tags must be used in the view: 224 |  --**{{expression}}, output the expression, and html formatted** 225 |  --**{* expression *}, the result of the expression expression is output as it is** 226 |  --**{% lua code%}, execute Lua code** 227 |  --**{# comments #} All content between {# and #} is considered commented (ie not output or executed)** 228 | 229 | Unlike the normal usage of the LUA-resty template, all view files are saved as LUA files in the sub-directory with the filename * .lua.html 230 | Rendering methods in your controller When you try to render, you get the content of the view as you need it 231 | 232 | views / layout directory storage frame view, rendering view default views / layout / main.lua.html, 233 | Controller can be set by the layout of the property to use a different frame view, such as: 234 | lua 235 | Local_M = require ("vendor.Controller"): new {layout = 'manage'} 236 | ``` 237 | Under the views of the other sub-directories for different functional modules corresponding to the content view, all page headers, page footers, menus and other content should be implemented in the frame view 238 | Other views are deprecated in the content view as {(templates)}, which can cause errors such as missing files due to different default renderings 239 | 240 | When the controller renders the view, it passes all application and controller data to the view so that it can be used directly in the view 241 | Such as: {{lang.siteName}} output the name of the site configured in the language pack 242 | 243 | In the view, you can set a contextual property to pass values ​​between the content view and the frame view, such as setting: 244 | {% Context.title = 'Development Description'%} 245 | You can display the {{title}} output in the frame view 246 | 247 | ## request processing 248 | ### Get request parameters 249 | lua 250 | Local request = require ("vendor.Request") 251 | --In the controller method can be obtained directly through self.request call 252 | 253 | local get = request.get() 254 | --equivalent to php: $get = $_GET; 255 | 256 | Local id = request.get ('id'); 257 | --equivalent to php: $id = isset ($_ GET ['id'])? $_GET ['id']: null; 258 | 259 | Local id = request.get ('id', 1) 260 | --equivalent to php: $id = isset ($_ GET ['id'])? $_GET ['id']: 1; 261 | 262 | Local post = request.post() 263 | --equivalent to php: $post = $_POST; 264 | 265 | Local name = request.post ('name') 266 | --equivalent to php: $name = isset ($_ POST ['name'])? $_POST ['name']: null; 267 | 268 | Local name = request.post ('name', '') 269 | --equivalent to php: $name = isset ($_ POST ['name'])? $_POST ['name']: ''; 270 | 271 | Local cookie = request.cookie() 272 | --equivalent to php: $cookie = $_COOKIE; 273 | 274 | Local sso = request.cookie ('sso') 275 | --equivalent to php: $sso = isset ($_ COOKIE ['sso'])? $_COOKIE ['sso']: null; 276 | 277 | Local sso = request.cookie ('sso', '') 278 | --equivalent to php: $sso = isset ($_ COOKIE ['sso'])? $_COOKIE ['sso']: ''; 279 | 280 |  --Determine whether there is an upload file 281 | ngx.say (request.isPostFile()) 282 |  --Note different places with PHP, if there is an upload file, the normal parameters can not be obtained through the request.post() method 283 |  --Recommended to upload files, common parameters passed by GET 284 | 285 |  --Receive uploaded files 286 | Local file = required ('vendor.Files') 287 | Local savename = path .. filename 288 | 289 | Local ok, err = file.receive ('upfile', savename) --receive upload file named upfile 290 | If not 291 | Return {retcode = 1, retmsg = 'Receive file failed, please try again:' .. err} 292 | End 293 | ``` 294 | ### Set cookie: 295 | ```lua 296 | Local util = needs "vendor.Util" 297 | util.set_cookie ('ABC', '123') 298 | util.set_cookie ('HD', '456', 3600) 299 | util.set_cookie (Name, Value, Expired, Path, Domain, Security, Http Only) 300 | ``` 301 | 302 | ### Session operation: 303 | ```lua 304 | ocal session = require "vendor.Session" 305 | --In the Controller method can be called directly through self.session 306 | --session.start() --Enable session, will set a _sessionid cookie, this operation can be omitted 307 | --session.exptime = 1800 --The default session time is 30 minutes 308 | --session.cache = ngx_shared ['session_cache'] --By default, ngx's cache corresponds to the lua_shared_dict configuration in runtime / nginx.conf 309 | --session.cache = require ("vendor.Memcache"): new {host = "127.0.0.1", port = "11211"} --Open this configuration with memcached cache 310 | 311 | local sessions = session.get() 312 | --equivalent to php: $ sessions = $ _SESSION; 313 | 314 | local abc = session.get ('abc') 315 | --equivalent to: local abc = session.abc 316 | --equivalent to: local abc = session ['abc'] 317 | --equivalent to php: $ abc = isset ($ _ SESSION ['abc'])? $ _SESSION ['abc']: null; 318 | 319 | local abc = session.get ('abc', '') 320 | --equivalent to php: $ abc = isset ($ _ SESSION ['abc'])? $ _SESSION ['abc']: ''; 321 | 322 | --set the session value 323 | session.set ('abc', 'abc-value') 324 | --equivalent to: session.abc = 'abc-value' 325 | --equivalent to: session ['abc'] = 'abc-value' 326 | ``` 327 | 328 | ## database operation (Working with Databases: 329 | 330 | ### Database Access (DAO) 331 | ```lua 332 | local db = require ('vendor.Mysql'): new { 333 |     host = "127.0.0.1", 334 |     port = 3306, 335 |     database = "mytest", 336 |     user = "root", 337 |     password = "", 338 |     charset = "utf8", --It is recommended to configure Mysql default connection character set utf8, you can get rid of this configuration item, this configuration will add a 'SET NAMES utf8' operation 339 | } 340 | 341 | --Or get it directly from the configuration file: 342 | local db = require ('config.db') 343 | 344 | --Execute sql: 345 | local rows = db: query ('select * from mytable') 346 | ngx.say (#rows) 347 | ``` 348 | ### Using Query Builder (Query Builder) 349 | vendor.Query encapsulates vendor.Mysql and provides a quick way to build a secure query, for example: 350 | ```lua 351 | local db = require ('config.db') 352 | local query = require ('vendor.Query')() 353 | local rows = query.use (db) .from ('products'). where ({product_id = 123}). all() 354 | --equivalent to executing local rows = db: query ("select * from products where product_id = '123'") 355 | ngx.say (query.get ('sql')) --can get to construct the sql statement 356 | 357 | --query.use() Used to specify the linked database 358 | query.use (require ('config.db')) 359 | 360 | --query.select() is used to specify the field to query, do not specify the default is select * 361 | query.select ({'id', 'email'}) 362 | --Equivalent to: 363 | query.select ('id, email') 364 | 365 | --query.from() is used to specify the table to be queried SELECT * FROM `user` 366 | query.from ('user') 367 | ``` 368 | query.where() is used to define the WHERE clause in the SQL statement. You can use the following three formats to define the WHERE condition: ** 369 | 370 | --** string format **, for example: 'status = 1', this method does not automatically add quotes or escapes. 371 | 372 | --** hash format **, for example: {status = 1, type = 2} This method will correctly quote the field name and escape the range of values 373 | 374 | --** operator format **, for example: {'in', {'2012', '2011'}} 375 |     The operator format allows you to specify any conditional statement of the class style, as follows: 376 |     {Operator, operand1, operand2, ...} 377 |     Each of these operands can be a string format, a hash format, or a nested operator format, and the operator can be one of the following: 378 | 379 |     Operator | usage 380 |     --------| -------------------------------------------- 381 |     ** and ** | Operands are concatenated with the AND keyword. For example, {'and', 'id = 1', 'id = 2'} will generate id = 1 AND id = 2. If the operand is an array, it is also converted to a string as described above. For example, {'and', 'type = 1', {'or', 'id = 1', 'id = 2'}} will generate type = 1 AND (id = 1 OR id = 2). This method does not automatically add quotes or escapes. 382 |     ** or ** | Similar to the and operator. 383 |     ** between ** | The first operand is the name of the field, and the second and third represent the range of values ​​for this field. For example, {'between', 'id', 1, 10} will generate id BETWEEN 1 AND 10. 384 |     ** not between ** | usage is similar to between 385 |     ** in ** | The first operand should be the field name, and the second operator is both an array. For example, {'in', 'id', {1, 2, 3}} will generate `id` IN ('1', '2', '3'). This method will correctly quote the field name and escape the value range 386 |     ** not in ** | Usage is similar to in operator. 387 | 388 | The query.orderBy() method is used to specify the ORDER BY clause in the SQL statement. ** 389 | For example, to achieve ... ORDER BY create_time desc can: 390 | ```lua 391 | query.orderBy ('create_time desc') 392 | --equivalent to: query.orderBy {create_time = desc} 393 | ``` 394 | 395 | The query.groupBy() method is used to specify the GROUP BY fragment in the SQL statement. ** 396 | For example, to achieve ... GROUP BY `id`,` status` could: 397 | ```lua 398 | query.groupBy {'id', 'status'} 399 | ``` 400 | 401 | The query.limit() and query.offset() are used to specify the LIMIT and OFFSET clauses in SQL statements. ** 402 | For example, to achieve ... LIMIT 10 OFFSET 20 Equivalent to mysql limit 20,10 Can: 403 | ```lua 404 | query.limit (10) .offset (20) 405 | ``` 406 | If you specify an invalid limit or offset (for example, a negative number), it will be ignored. 407 | 408 | 409 | ** query.page() can set LIMIT and OFFSET for paging query by passing "vendor.Pager" object: ** 410 | ```lua 411 | local page = require ("vendor.Pager") { 412 |     pageSize = 8, 413 |     totalCount = 10, 414 | } 415 | query.page (page) --Equivalent to: query.limit (page.limit) .offset (page.offset) 416 | ``` 417 | #### Query method 418 | vendor.Query provides a complete set of methods for different query purposes. 419 | query.all(): will return an array of rows. 420 | query.one(): returns the first row of the result set. 421 | query.count(): returns the result of a COUNT query. 422 | 423 | E.g: 424 | ```lua 425 | local db = require ('config.db') 426 | local query = require ('vendor.Query') {}. use (db) .from ('products'). where {product_id = 123} 427 | local rows = query.all() 428 | local row = query.one() 429 | local count = query.count() 430 | ``` 431 | 432 | Other methods: 433 | query.insert(): Insert data. 434 | query.update(): update the data. 435 | query.delete(): delete data. 436 | query.save(): The new data call is equivalent to query.insert() and the query result set call is equivalent to query.update(). 437 | 438 | E.g: 439 | ```lua 440 | local db = require ('config.db') 441 | local data = {product_id = 123, product_name = 'name123'} 442 | local query = require ('vendor.Query') (data) .use (db) .from ('products') 443 | 444 | --insert: 445 | local row = query.insert() 446 | --To get the self-growth id of newly inserted data, use: 447 | local id = row.get ('insert_id') --equivalent to query.get ('insert_id') 448 | 449 | --Updated: 450 | row.product_name = 'name456' 451 | row.update() --or query.update (row) 452 | 453 | --delete: 454 | row.delete() --or query.update (row) 455 | ``` 456 | 457 | ### Active Record 458 | vendor.ActiveRecord further encapsulates the query generator vendor.Query 459 | At the same time, vendor.ActiveRecord inherits from vendor.Model and can use the load(), rules(), hasErrors(), validate() methods of the model object 460 | 461 | Example of use 462 | ```lua 463 | local Product = require ('vendor.ActiveRecord') { 464 |     --db = require ('config.db'), --optional attribute, specify the database connection to use, default 'config.db' 465 |     tableName = function() --The tableName method must be implemented to return the name of the data table to manipulate 466 |         return 'products' 467 |     end 468 | } 469 | 470 | --insert data: 471 | local product = Product.new { 472 |     product_id = 123, 473 |     product_name = 'name123', 474 | } --The returned object supports using the vendor.Query method 475 | product.save() --equivalent to product.insert() 476 | 477 | --Quickly find a line 478 | local row = Product.findOne {product_id = 123} --The returned object supports using the vendor.Query method 479 | 480 | --Quickly find multiple lines 481 | local rows = Product.findAll {user = 'creater'} --The returned object supports using the vendor.Query method 482 | 483 | --Complex queries 484 | local query = Product.find(). where {user = 'creater'} --The returned object supports using the vendor.Query method 485 | local page = Pager { 486 |     pageSize = 8, 487 |     totalCount = query.count(), 488 | } 489 | local list = query.orderBy ('create_time desc'). page (page) .all() 490 | 491 | --You can also do the following 492 | query.insert() --insert data. 493 | query.update() --Update the data. 494 | query.delete() --delete the data. 495 | ``` 496 | It is recommended that each data table be created in the models directory as an operation class that inherits from 'vendor.ActiveRecord' 497 | 498 | ### Database security issues 499 | Use vendor.ActiveRecord or vendor.Query to automatically generate query statements 500 | Sql statement in the construction process, if the parameters passed is a form, the constructor will be escaped operation to prevent sql injection 501 | 502 | But if the string is passed, it will not escape operation, it is recommended to minimize the use of, and to ensure that sql security 503 | Query.get ('sql') can be obtained by the implementation of the sql statement 504 | 505 | If you want to escape, you can use the util.mescape() method 506 | ```lua 507 | local util = require ("vendor.Util") 508 | value = util.mescape (value) 509 | ``` 510 | 511 | lua-resty-yii is available under the MIT license. See the [LICENSE file][1] 512 | for more information. 513 | 514 | [1]: ./LICENSE.txt -------------------------------------------------------------------------------- /web/css/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | .btn-default, 7 | .btn-primary, 8 | .btn-success, 9 | .btn-info, 10 | .btn-warning, 11 | .btn-danger { 12 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); 13 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 14 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 15 | } 16 | .btn-default:active, 17 | .btn-primary:active, 18 | .btn-success:active, 19 | .btn-info:active, 20 | .btn-warning:active, 21 | .btn-danger:active, 22 | .btn-default.active, 23 | .btn-primary.active, 24 | .btn-success.active, 25 | .btn-info.active, 26 | .btn-warning.active, 27 | .btn-danger.active { 28 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 29 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 30 | } 31 | .btn-default.disabled, 32 | .btn-primary.disabled, 33 | .btn-success.disabled, 34 | .btn-info.disabled, 35 | .btn-warning.disabled, 36 | .btn-danger.disabled, 37 | .btn-default[disabled], 38 | .btn-primary[disabled], 39 | .btn-success[disabled], 40 | .btn-info[disabled], 41 | .btn-warning[disabled], 42 | .btn-danger[disabled], 43 | fieldset[disabled] .btn-default, 44 | fieldset[disabled] .btn-primary, 45 | fieldset[disabled] .btn-success, 46 | fieldset[disabled] .btn-info, 47 | fieldset[disabled] .btn-warning, 48 | fieldset[disabled] .btn-danger { 49 | -webkit-box-shadow: none; 50 | box-shadow: none; 51 | } 52 | .btn-default .badge, 53 | .btn-primary .badge, 54 | .btn-success .badge, 55 | .btn-info .badge, 56 | .btn-warning .badge, 57 | .btn-danger .badge { 58 | text-shadow: none; 59 | } 60 | .btn:active, 61 | .btn.active { 62 | background-image: none; 63 | } 64 | .btn-default { 65 | text-shadow: 0 1px 0 #fff; 66 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); 67 | background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); 68 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); 69 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); 70 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 71 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 72 | background-repeat: repeat-x; 73 | border-color: #dbdbdb; 74 | border-color: #ccc; 75 | } 76 | .btn-default:hover, 77 | .btn-default:focus { 78 | background-color: #e0e0e0; 79 | background-position: 0 -15px; 80 | } 81 | .btn-default:active, 82 | .btn-default.active { 83 | background-color: #e0e0e0; 84 | border-color: #dbdbdb; 85 | } 86 | .btn-default.disabled, 87 | .btn-default[disabled], 88 | fieldset[disabled] .btn-default, 89 | .btn-default.disabled:hover, 90 | .btn-default[disabled]:hover, 91 | fieldset[disabled] .btn-default:hover, 92 | .btn-default.disabled:focus, 93 | .btn-default[disabled]:focus, 94 | fieldset[disabled] .btn-default:focus, 95 | .btn-default.disabled.focus, 96 | .btn-default[disabled].focus, 97 | fieldset[disabled] .btn-default.focus, 98 | .btn-default.disabled:active, 99 | .btn-default[disabled]:active, 100 | fieldset[disabled] .btn-default:active, 101 | .btn-default.disabled.active, 102 | .btn-default[disabled].active, 103 | fieldset[disabled] .btn-default.active { 104 | background-color: #e0e0e0; 105 | background-image: none; 106 | } 107 | .btn-primary { 108 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); 109 | background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); 110 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); 111 | background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); 112 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); 113 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 114 | background-repeat: repeat-x; 115 | border-color: #245580; 116 | } 117 | .btn-primary:hover, 118 | .btn-primary:focus { 119 | background-color: #265a88; 120 | background-position: 0 -15px; 121 | } 122 | .btn-primary:active, 123 | .btn-primary.active { 124 | background-color: #265a88; 125 | border-color: #245580; 126 | } 127 | .btn-primary.disabled, 128 | .btn-primary[disabled], 129 | fieldset[disabled] .btn-primary, 130 | .btn-primary.disabled:hover, 131 | .btn-primary[disabled]:hover, 132 | fieldset[disabled] .btn-primary:hover, 133 | .btn-primary.disabled:focus, 134 | .btn-primary[disabled]:focus, 135 | fieldset[disabled] .btn-primary:focus, 136 | .btn-primary.disabled.focus, 137 | .btn-primary[disabled].focus, 138 | fieldset[disabled] .btn-primary.focus, 139 | .btn-primary.disabled:active, 140 | .btn-primary[disabled]:active, 141 | fieldset[disabled] .btn-primary:active, 142 | .btn-primary.disabled.active, 143 | .btn-primary[disabled].active, 144 | fieldset[disabled] .btn-primary.active { 145 | background-color: #265a88; 146 | background-image: none; 147 | } 148 | .btn-success { 149 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 150 | background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); 151 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); 152 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 153 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 154 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 155 | background-repeat: repeat-x; 156 | border-color: #3e8f3e; 157 | } 158 | .btn-success:hover, 159 | .btn-success:focus { 160 | background-color: #419641; 161 | background-position: 0 -15px; 162 | } 163 | .btn-success:active, 164 | .btn-success.active { 165 | background-color: #419641; 166 | border-color: #3e8f3e; 167 | } 168 | .btn-success.disabled, 169 | .btn-success[disabled], 170 | fieldset[disabled] .btn-success, 171 | .btn-success.disabled:hover, 172 | .btn-success[disabled]:hover, 173 | fieldset[disabled] .btn-success:hover, 174 | .btn-success.disabled:focus, 175 | .btn-success[disabled]:focus, 176 | fieldset[disabled] .btn-success:focus, 177 | .btn-success.disabled.focus, 178 | .btn-success[disabled].focus, 179 | fieldset[disabled] .btn-success.focus, 180 | .btn-success.disabled:active, 181 | .btn-success[disabled]:active, 182 | fieldset[disabled] .btn-success:active, 183 | .btn-success.disabled.active, 184 | .btn-success[disabled].active, 185 | fieldset[disabled] .btn-success.active { 186 | background-color: #419641; 187 | background-image: none; 188 | } 189 | .btn-info { 190 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 191 | background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 192 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); 193 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 194 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 195 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 196 | background-repeat: repeat-x; 197 | border-color: #28a4c9; 198 | } 199 | .btn-info:hover, 200 | .btn-info:focus { 201 | background-color: #2aabd2; 202 | background-position: 0 -15px; 203 | } 204 | .btn-info:active, 205 | .btn-info.active { 206 | background-color: #2aabd2; 207 | border-color: #28a4c9; 208 | } 209 | .btn-info.disabled, 210 | .btn-info[disabled], 211 | fieldset[disabled] .btn-info, 212 | .btn-info.disabled:hover, 213 | .btn-info[disabled]:hover, 214 | fieldset[disabled] .btn-info:hover, 215 | .btn-info.disabled:focus, 216 | .btn-info[disabled]:focus, 217 | fieldset[disabled] .btn-info:focus, 218 | .btn-info.disabled.focus, 219 | .btn-info[disabled].focus, 220 | fieldset[disabled] .btn-info.focus, 221 | .btn-info.disabled:active, 222 | .btn-info[disabled]:active, 223 | fieldset[disabled] .btn-info:active, 224 | .btn-info.disabled.active, 225 | .btn-info[disabled].active, 226 | fieldset[disabled] .btn-info.active { 227 | background-color: #2aabd2; 228 | background-image: none; 229 | } 230 | .btn-warning { 231 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 232 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 233 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); 234 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 235 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 236 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 237 | background-repeat: repeat-x; 238 | border-color: #e38d13; 239 | } 240 | .btn-warning:hover, 241 | .btn-warning:focus { 242 | background-color: #eb9316; 243 | background-position: 0 -15px; 244 | } 245 | .btn-warning:active, 246 | .btn-warning.active { 247 | background-color: #eb9316; 248 | border-color: #e38d13; 249 | } 250 | .btn-warning.disabled, 251 | .btn-warning[disabled], 252 | fieldset[disabled] .btn-warning, 253 | .btn-warning.disabled:hover, 254 | .btn-warning[disabled]:hover, 255 | fieldset[disabled] .btn-warning:hover, 256 | .btn-warning.disabled:focus, 257 | .btn-warning[disabled]:focus, 258 | fieldset[disabled] .btn-warning:focus, 259 | .btn-warning.disabled.focus, 260 | .btn-warning[disabled].focus, 261 | fieldset[disabled] .btn-warning.focus, 262 | .btn-warning.disabled:active, 263 | .btn-warning[disabled]:active, 264 | fieldset[disabled] .btn-warning:active, 265 | .btn-warning.disabled.active, 266 | .btn-warning[disabled].active, 267 | fieldset[disabled] .btn-warning.active { 268 | background-color: #eb9316; 269 | background-image: none; 270 | } 271 | .btn-danger { 272 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 273 | background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 274 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); 275 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 276 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 277 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 278 | background-repeat: repeat-x; 279 | border-color: #b92c28; 280 | } 281 | .btn-danger:hover, 282 | .btn-danger:focus { 283 | background-color: #c12e2a; 284 | background-position: 0 -15px; 285 | } 286 | .btn-danger:active, 287 | .btn-danger.active { 288 | background-color: #c12e2a; 289 | border-color: #b92c28; 290 | } 291 | .btn-danger.disabled, 292 | .btn-danger[disabled], 293 | fieldset[disabled] .btn-danger, 294 | .btn-danger.disabled:hover, 295 | .btn-danger[disabled]:hover, 296 | fieldset[disabled] .btn-danger:hover, 297 | .btn-danger.disabled:focus, 298 | .btn-danger[disabled]:focus, 299 | fieldset[disabled] .btn-danger:focus, 300 | .btn-danger.disabled.focus, 301 | .btn-danger[disabled].focus, 302 | fieldset[disabled] .btn-danger.focus, 303 | .btn-danger.disabled:active, 304 | .btn-danger[disabled]:active, 305 | fieldset[disabled] .btn-danger:active, 306 | .btn-danger.disabled.active, 307 | .btn-danger[disabled].active, 308 | fieldset[disabled] .btn-danger.active { 309 | background-color: #c12e2a; 310 | background-image: none; 311 | } 312 | .thumbnail, 313 | .img-thumbnail { 314 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 315 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 316 | } 317 | .dropdown-menu > li > a:hover, 318 | .dropdown-menu > li > a:focus { 319 | background-color: #e8e8e8; 320 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 321 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 322 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 323 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 324 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 325 | background-repeat: repeat-x; 326 | } 327 | .dropdown-menu > .active > a, 328 | .dropdown-menu > .active > a:hover, 329 | .dropdown-menu > .active > a:focus { 330 | background-color: #2e6da4; 331 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 332 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 333 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 334 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 335 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 336 | background-repeat: repeat-x; 337 | } 338 | .navbar-default { 339 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); 340 | background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); 341 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); 342 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); 343 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 344 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 345 | background-repeat: repeat-x; 346 | border-radius: 4px; 347 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 348 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 349 | } 350 | .navbar-default .navbar-nav > .open > a, 351 | .navbar-default .navbar-nav > .active > a { 352 | background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 353 | background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 354 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); 355 | background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); 356 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); 357 | background-repeat: repeat-x; 358 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 359 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 360 | } 361 | .navbar-brand, 362 | .navbar-nav > li > a { 363 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25); 364 | } 365 | .navbar-inverse { 366 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); 367 | background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); 368 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); 369 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); 370 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 371 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 372 | background-repeat: repeat-x; 373 | border-radius: 4px; 374 | } 375 | .navbar-inverse .navbar-nav > .open > a, 376 | .navbar-inverse .navbar-nav > .active > a { 377 | background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); 378 | background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); 379 | background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); 380 | background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); 381 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); 382 | background-repeat: repeat-x; 383 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 384 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 385 | } 386 | .navbar-inverse .navbar-brand, 387 | .navbar-inverse .navbar-nav > li > a { 388 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); 389 | } 390 | .navbar-static-top, 391 | .navbar-fixed-top, 392 | .navbar-fixed-bottom { 393 | border-radius: 0; 394 | } 395 | @media (max-width: 767px) { 396 | .navbar .navbar-nav .open .dropdown-menu > .active > a, 397 | .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, 398 | .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { 399 | color: #fff; 400 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 401 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 402 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 403 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 404 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 405 | background-repeat: repeat-x; 406 | } 407 | } 408 | .alert { 409 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2); 410 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 411 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 412 | } 413 | .alert-success { 414 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 415 | background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 416 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); 417 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 418 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 419 | background-repeat: repeat-x; 420 | border-color: #b2dba1; 421 | } 422 | .alert-info { 423 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 424 | background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 425 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); 426 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 427 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 428 | background-repeat: repeat-x; 429 | border-color: #9acfea; 430 | } 431 | .alert-warning { 432 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 433 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 434 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); 435 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 436 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 437 | background-repeat: repeat-x; 438 | border-color: #f5e79e; 439 | } 440 | .alert-danger { 441 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 442 | background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 443 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); 444 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 445 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 446 | background-repeat: repeat-x; 447 | border-color: #dca7a7; 448 | } 449 | .progress { 450 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 451 | background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 452 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); 453 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 454 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 455 | background-repeat: repeat-x; 456 | } 457 | .progress-bar { 458 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); 459 | background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); 460 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); 461 | background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); 462 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); 463 | background-repeat: repeat-x; 464 | } 465 | .progress-bar-success { 466 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 467 | background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); 468 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); 469 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 470 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 471 | background-repeat: repeat-x; 472 | } 473 | .progress-bar-info { 474 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 475 | background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 476 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); 477 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 478 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 479 | background-repeat: repeat-x; 480 | } 481 | .progress-bar-warning { 482 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 483 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 484 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); 485 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 486 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 487 | background-repeat: repeat-x; 488 | } 489 | .progress-bar-danger { 490 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 491 | background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); 492 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); 493 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 494 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 495 | background-repeat: repeat-x; 496 | } 497 | .progress-bar-striped { 498 | background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 499 | background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 500 | background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 501 | } 502 | .list-group { 503 | border-radius: 4px; 504 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 505 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 506 | } 507 | .list-group-item.active, 508 | .list-group-item.active:hover, 509 | .list-group-item.active:focus { 510 | text-shadow: 0 -1px 0 #286090; 511 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); 512 | background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); 513 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); 514 | background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); 515 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); 516 | background-repeat: repeat-x; 517 | border-color: #2b669a; 518 | } 519 | .list-group-item.active .badge, 520 | .list-group-item.active:hover .badge, 521 | .list-group-item.active:focus .badge { 522 | text-shadow: none; 523 | } 524 | .panel { 525 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 526 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 527 | } 528 | .panel-default > .panel-heading { 529 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 530 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 531 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 532 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 533 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 534 | background-repeat: repeat-x; 535 | } 536 | .panel-primary > .panel-heading { 537 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 538 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 539 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 540 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 541 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 542 | background-repeat: repeat-x; 543 | } 544 | .panel-success > .panel-heading { 545 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 546 | background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 547 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); 548 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 549 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 550 | background-repeat: repeat-x; 551 | } 552 | .panel-info > .panel-heading { 553 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 554 | background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 555 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); 556 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 557 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 558 | background-repeat: repeat-x; 559 | } 560 | .panel-warning > .panel-heading { 561 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 562 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 563 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); 564 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 565 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 566 | background-repeat: repeat-x; 567 | } 568 | .panel-danger > .panel-heading { 569 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 570 | background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 571 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); 572 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 573 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 574 | background-repeat: repeat-x; 575 | } 576 | .well { 577 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 578 | background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 579 | background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); 580 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 581 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 582 | background-repeat: repeat-x; 583 | border-color: #dcdcdc; 584 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 585 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 586 | } 587 | /*# sourceMappingURL=bootstrap-theme.css.map */ 588 | -------------------------------------------------------------------------------- /web/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.5",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.5",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),a(c.target).is('input[type="radio"]')||a(c.target).is('input[type="checkbox"]')||c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.5",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.5",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.5",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger("shown.bs.dropdown",h)}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),c.isInStateTrue()?void 0:(clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide())},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.5",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.5",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.5",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); --------------------------------------------------------------------------------