# 进阶功能

# 模型扩展

模型是项目开发中一个重要的部分,框架提供了模型功能,为此我们方便操作数据库。本文主要介绍项目自定义模型基类的常用的方法。自定义模型基类可以让我们更好的对数据进行增删改查操作,基类会自动加入基本数据,如加入添加时间,主键id等

1.模型中使用

如下所示是在模型中调用封装的自定义模型基类,


//注意正常情况下如果表名和模型一致是不用设置表名的,
//也可以通过tableName配置模型表名,具体使用方式查看thinkjs文档

//使用封装基类
const BaseModel = require('./utils/lgBase');
module.exports = class  extends BaseModel {
    	// 设置表名(建议用这种方法,但是要注意需要自己手动写上表名前缀)
	//  get tableName() {
	//    return 'zee_device'
	//  }
    
		// 添加
 setAdd() {
    return this.table('device').add({id_device_type: '测试添加'});
  }
    // 修改
    setUpdate() {
       return this.table('device').where({id:'431887248539781'})
	   .update({id_device_type: '修改测试添加'});
     }
     // 删除(逻辑删除)
     setDelete() {
    return this.table('device').where({id:'431887248539781'}).delete();
      }
  	// 分页查询
     getCountSelect() {
       return this.table('device').page(1).countSelect()
     }
}

//---------------------------------------------------------------

//使用原生方法

module.exports = class  extends think.Model{
     	// 设置表名(建议用这种方法,但是要注意需要自己手动写上表名前缀)
	//  get tableName() {
	//    return 'zee_device'
	//  }
	// 添加
 setAdd() {
    return this.table('device')
	.add({id:"123",create_by:think.User.id,create_at:think.datetime(),
	id_device_type: '测试添加'});
  }
  // 修改
  setUpdate() {
     return this.table('device').where({id:'123'})
	 .update({updated_by:think.User.id,updated_at:think.datetime(),
	 id_device_type: '修改测试添加'});
   }
   // 删除(逻辑删除)
   setDelete() {
  return this.table('device').where({id:'431887248539781'})
  .update({delete_by:think.User.id,delete_at:think.datetime(),is_rec:true});
    }
	// 分页查询
   getCountSelect() {
	   // 注,如果使用原生方法,模型名称和表名不一致无法使用countSelect,
	   //因为使用countSelect不可以设置table
     return this.page(1).countSelect()
   }
}

注意事项

如果在继承的自定义基类中复写了after钩子需要调用基类的他们方法,具体方法如下所示

const BaseModel = require('./utils/lgBase');
module.exports = class  extends BaseModel {
 async afterFind(...args) {
    await super.afterFind(...args);
    //do something...
  }
}

2.其他地方使用

如下所示是在控制层中调用封装的自定义模型基类,通过调用think.modelPro方法可以使用自定义基类


module.exports = class extends  think.Controller {
 async indexAction() {
 let data = await  think.modelPro('device')
 .setUpdate({id_device_type: '测试添加2'})
		 return this.success(data);
  }
};
/**
 * 可传入值
  * @param {String} 表名
  * @param {obj} 修改并添加其他的模型实例参数
 */

注意事项

1.使用 think.modelPro()不能访问model里面的模型文件,只能访问表模型。

2.使用 think.model()可以访问model里面的模型文件,也可以访问表模型,但是直接访问表模型,则无法使用扩展基类。

3.推荐直接访问表模型时使用think.modelPro(),访问模型文件时,使用think.model()。

如果你使用了自定义模型基类,但又不想自定义基类默认加入参数,如在查询时默认筛选is_rec,可使用下面方法


module.exports = class extends  think.Controller {
 async indexAction() {
 let data = await  think.modelPro('device'{no_rec:true,no_create:true})
 .setUpdate({id_device_type: '测试添加2'})
 
 let data1 = await  think.model('device'{no_rec:true,no_create:true})
 .setUpdate({id_device_type: '测试添加2'})
		 return this.success(data);
  }
};

//no_rec 为true,则表示不过滤逻辑删除的数据
//no_create 为true,则表示默认不进行添加时间排序


# 拦截配置

有时候我们希望某些接口不进行token等检验,所以本篇主要介绍如何在项目在对接口进行放行。 首先我们进入中间件auth文件,在这里进行配置拦截。我们使用正则进行拦截匹配,下面我会对4个拦截方法进行介绍。

匹配规则

1.在正则中如果要把特殊字符作为常规字符来使用,只需要在它前面加个反斜杠 \, 所以在我们匹配的路径中出现特殊符合如/ ,则需要进行使用反斜杠进行转义。

2.如果想要匹配多个路径则使用|进行拼接。

3.条件不能为空,否则会认为没有条件,匹配所有。

条件不能为空,否则会认为没有条件,匹配所有。

条件不能为空,否则会认为没有条件,匹配所有。

重要的事情说三遍

  • one_matching 第一级开始匹配
 //第一级开始匹配
 
//假设我们的路径是 /web/school/teacher/list,如果需要让符合/web/school进行放行则如下设置
//需要注意括号不能为空,也不能设置|后面为空,否则正则会认为你没有条件
	const one_matching= /^\/(web\/school|demo1)/  
  • two_matching 第二级开始匹配
 // 第二级开始匹配
//假设我们的路径是 /web/school/teacher/list,如果需要让符合/school/teacher进行放行则如下设置
//需要注意括号不能为空,也不能设置|后面为空,否则正则会认为你没有条件
	const one_matching= /\/\w+\/(school\/teacher|demo2)/
  • finally_matching 最后一级往前匹配
 //最后一级往前匹配,但不匹配最前面的一级
//假设我们的路径是 /web/school/teacher/list,如果需要让符合list进行放行则如下设置
	const one_matching= /\/[\w-]+\/(list)$/  
  • wilfully_matching 任意匹配
 //任意匹配
//假设我们的路径是 /web/school/teacher/list,如果需要让符合school进行放行则如下设置
//需要注意括号不能为空,也不能设置|后面为空,否则正则会认为你没有条件
	const wilfully_matching= /\/(school)+/ 

案例:


module.exports = (options) => {
	return async (ctx, next) => {
		//需要注意括号不能为空,,也不能设置|后面为空,否则正则会认为你没有条件
      //第一级开始匹配
	const one_matching= /^\/(common\/socket)/  
	 // 第二级开始匹配
	const two_matching= /\/\w+\/(school|~)/
	// 最后一级往前匹配,但不匹配最前面的一级
	const finally_matching=  /\/[\w-]+\/(school\/teacher\/list)$/
	 //任意匹配
	const wilfully_matching= /\/(~)+/ 
	
		if(one_matching.test(ctx.url)||two_matching.test(ctx.url)||
		finally_matching.test(ctx.url)||wilfully_matching.test(ctx.url))
		{
		await next();
		}else{
			const {token,source,version}=ctx.headers
				if(source&&version&&version=='1.0'){
		if(!token) 	return ctx.fail(403, '请登录')
		let token_check= await think.service('utils/utils').verifyToken(token)	
		if(!token_check) return ctx.fail(401, '无效登录或登录已经过期,请重新登录')
		let user={id:111}
		if(source=='app'){
		// user = await app.mysql.get('v_seg__user_login', { token:authorization});
		}else if(source=='web'){
		// user = await app.mysql.get('v_seg__user_login', { token:authorization});
		}
		if (think.isEmpty(user)){
		ctx.fail(401,'你已被登出,请重新登录');
		return false;
		} else {
		think.User= await {id:1111}
		await next();
		}
		}else{
		ctx.fail(500, '请求错误');
		return false;
			}
		}
	
	}
};

# 定时任务

项目在线上运行时,经常要定时去执行某个功能(如:定时去远程拉取一些数据、定时计算数据库里的一些数据进行汇总),这时候就需要使用定时任务来处理了。

定时任务的配置文件为src/config/crontab.js

在开始配置之前我们先了解一下crontab 的格式,因为正常开发中我们通常并不会使用定时器去执行定时任务, 使用 cron 参数来配置定时任务的执行时机,定时任务将会按照 cron 表达式在特定的时间点执行。

 *    *    *    *    * 
-    -    -    -    -
|    |    |    |    |
|    |    |    |    ----- 星期几(0 - 7)(星期日为 07|    |    |    ------- 月份(1 - 12|    |    --------- 日(1 - 31|    ----------- 小时(0 - 23----------------- 分钟(0 - 59

配置案例

module.exports = [
	{
    cron: '50 23 * * *',
    handle: async () => {
        think.logger.info('开启更新体温统计定时任务',think.datetime());
        try {
            await think.service('task/task').updateTemperatureCount();
        } catch (e) {
            think.logger.error(e);
        }
    }
},
{
    cron: '0 0 * * *',
    handle:'crontab/test'
}]

/**
 * 可传入值
  * @param {String | Number} interval  执行的时间间隔
  * @param {String}  cron   crontab 的格式
  * @param {String}  type   任务执行方式, one 或者 all, 默认是 one
  * @param {String}  handle  执行任务,执行相应函数或者是路由地址
  * @param {String}  immediate   是否在启动项目时就立即执行一次,默认是 false
  * @param {String}  enable  是否开启,默认是 true
 */

# 数据校验

项目开发中我们需要对用户提交的数据进行校验,所有我们需要通过Logic进行校验。 Logic 目录在 src/logic,我们只需要保证Logic 里的 Action 和控制器里的 Action 一一对应, 系统在调用控制器里的 Action 之前会自动调用 Logic 里的 Action。本文主要介绍常用的和一些扩展的校验方法,更多使用方法请查看Logic (opens new window)

案例1

 module.exports = class extends think.Logic {
   indexAction(){
     this.rules = {
          username: {
           required: true,     // 字段必填
     }
   }
 }
 //此时如果获取不到则会返回输出提示username不能为空

案例1

 module.exports = class extends think.Logic {
   indexAction(){
     this.rules = {
          username: {
          regexp:/^1[3|4|5|6|7|8|9][0-9]\d{8}$/, //正则
           aliasName:'手机号码错误' //输出提示
     }
   }
 }
 //此时如果正则检验失败则会返回输出提示手机号码错误

案例3

 module.exports = class extends think.Logic {
   indexAction(){
     this.rules = {
          username: {
         validator:(rule, value,data)=>{
         	return value==123
         }, //自定义方法
           aliasName:'检验失败' //输出提示
     }
   }
 }
 //此时如果正则检验失败则会返回输出提示检验失败
 
 /**
  * 可调用值
   * @param {obj} rule 当前校验字段在 rules 中所对应的校验规则
   * @param {String}  value  当前校验字段的值
   * @param {obj}  data 获取所以字段值
   * @return {Boolean}  输出结果,true为通过,false为不通过
  */
 

注意事项

1.required会在提示后面加上不能为空

2.validator和regexp需要传入aliasName输出检验提示,否则显示为字段名。

3.其他方法则按框架默认提示。

# Redis

开发中。。

# 通信功能

# Socket.IO

一、使用Socket.IO之前请确定项目中是否安装了这两个依赖包


   npm install think-websocket
   npm install think-websocket-socket.io

二、src/config/adapter.js 中配置如下

const socketio = require('think-websocket-socket.io');

exports.websocket = {
  type: 'socketio',
  common: {
    // common config 配置通用的一些参数,项目启动时会跟具体的 adapter 参数作合并
  },
  socketio: {
    handle: socketio, 
// allowOrigin:'127.0.0.1:8360',  // 默认所有的域名都允许访问
    path: '/socket.io',      // 默认 '/socket.io'
    adapter: null,     // 默认无 adapter 
    messages: {
    open: '/common/socket/open', //建立连接时处理对应到 websocket Controller 下的 open Action
     close: '/common/socket/close',     // 关闭连接时处理的 Action
         addUser: '/common/socket/addUser', // addUser 事件处理的 Action
    }
  }
}
// 其中 open 和 close 事件名固定,表示建立连接和断开连接的事件,
//其他事件均为自定义,项目里可以根据需要添加

/**
 * 可传入值
  * @param {String} allowOrigin  默认情况下 socket.io 允许所有域名的访问。可配置
  * @param {String}  path  设置被 socket.io 处理的路径,默认为 /socket.io
  * @param {String}  adapter   使用多节点来部署 WebSocket 时,多节点之间可以借助 Redis
   *                            进行通信,这时可以设置 adapter 来实现。
  * @param {String}  messages   响应事件处理
 */

三、src/config/extend.js 中配置如下

const websocket = require('think-websocket');
module.exports = [
	websocket(think.app),
 // ...
];

四、src/config/router.js中配置如下

module.exports = [
	['/common/socket/:action','/common/socket/:action','get,websocket'],
];

五、编写控制层文件

module.exports = class extends think.Controller {

  constructor(...arg) {
    super(...arg);
  }

  openAction() {
	  console.log('开启了');

 //    this.emit('opend', 'This client opened successfully!')
    // this.broadcast('joined', 'There is a new client joined successfully!')
  }
  openAction() {
	  console.log('关闭了');
   //  this.emit('opend', 'This client opened successfully!')
    // this.broadcast('joined', 'There is a new client joined successfully!')
  }
  addUserAction() {
    console.log('获取客户端 addUser 事件发送的数据', this.wsData);
    // console.log('获取当前 WebSocket 对象', this.websocket);
    console.log('判断当前请求是否是 WebSocket 请求', this.isWebsocket);
  }
}

六、常用命令

 //通过messages触发的事件可以通过下面方法 发送事件
 this.emit('opend', 'This client opened successfully!')
 
 
 //在 Action 里可以通过 this.ctx.req.io/this.ctx.app.websocket.io 来获取 io 对象,
 //该对象为 socket.io 的一个实例
 
 const socket = this.ctx.app.websocket.io; 
 socket.emit('carNum',"This client opened successfully!");

// 监听
 const socket = this.ctx.app.websocket.io; 
socket.on('test', function(socket){
  console.log('a user connected');
});

// 关闭监听
 const socket = this.ctx.app.websocket.io; 
socket.off('test', function(socket){
  console.log('a user connected');
});


// broadcast方法给所有的 socket 广播事件
 const socket = this.ctx.app.websocket.io; 
socket.broadcast('test', function(socket){
  console.log('a user connected');
});


# mqtt

# 1.使用mqtt作为服务端

由于实际项目中很少使用该项目作为服务端,所以本文只介绍一些比较简单的使用方法,更多使用方法见mosca (opens new window)

一、安装mqtt 服务器必要依赖项 mosca

npm install mosca

二、在根目录下创建 script/mosca.js,并编写如下代码。

let mosca = require('mosca');

let settings = {
  port: 5112, //mqtt端口号
};

let server = new mosca.Server(settings);
  // 监听启动
server.on('ready', function() {
	// 判断用户密码
	server.authenticate = function(client, username, password, callback) {
	    var authorized = (username === 'alice' && password.toString() === 'secret');
	    if (authorized) client.user = username;
	    callback(null, authorized);
	  }
  console.log('Mosca server is up and running');
});

server.on('clientConnected', function(client) {
  console.log('Client connected: ', client.id);
});

server.on('clientDisconnected', function(client) {
  console.log('Client disconnected: ', client.id);
});
// 事件监听
server.on('published', function(packet, client) {
    console.log(packet.payload.toString());
  console.log('Published: ', packet.payload);
});

三、在 package.json中添加一条新的启动命令。

  "scripts": {
   ...
	"mosca": "node script/mosca.js",
  },

四、打开一个新的命令窗口启动


npm run mosca

注意事项

若启动报错报错SchemaError: Expected schema to be an object or boolean有以下两种解决方法。

1.找到错误地址所在文件,将错误的的三行代码进行注释。

2.将jsonschema版本安装为1.2.6

# 2.使用mqtt作为客户端

一、 安装依赖

npm install mqtt

二、创建连接

let mqtt = require('mqtt')

module.exports = class extends  think.Controller {

 async listAction() {
	 const host = '127.0.0.1'
	 const port = '5112'
	 const clientId = `mqtt_${Math.random().toString(16).slice(3)}`
	 const connectUrl = `mqtt://${host}:${port}`
	 // 基础配置
const client =  mqtt.connect(connectUrl, {
	     clientId: clientId ,
	     clean: true,
	      connectTimeout: 4000,
	      username: 'alice',
	      password: 'secret',
	      reconnectPeriod: 1000,
	 })
	 
	 // 监听连接
	 client.on('connect', () => {
	   console.log('连接成功')
	 })
	 
	 // 发布mqtt消息
	 client.publish('callNum', '32222222')

     // 订阅消息
     client.subscribe('topic1', function (err) {
     if (!err) {
    client.on('message', function (topic, message) {
      // message is Buffer
      console.log(message.toString())
      })
     }
     })
	 
	 // 取消订阅
    client.unsubscribe('topic1');
	
	// 关闭连接
	client.end(function () {
	  console.log('disconnected');
	});
		 return this.success("data");
  }
};