蒋海云个人博客,日拱一卒。 2021-02-18T11:24:09+08:00 jiang.haiyun#gmail.com Html 中各种宽高尺寸汇总 2020-06-08T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/html-height-width-intro 1. window innerHeight 和 outerHeight

innerHeight 表示浏览器中页面内部呈现部分的高度,包括水平滚动条部分(如果有的话),不包括页面标签部分。

outerHeight 表示整个浏览器的高度。

outerHeight and innerHeight

参考 Window innerHeight

2. Element.clientHeight

元素内容在 box 中呈现部分高度:包括 padding,但不包括 borders, margins, horizontal scrollbars。

clientHeight = CSS height + CSS padding - height of horizontal scrollbar。

document height 等同于 html.clientHeight 或 body.clientHeight。

Dimensions-client.png

参考 Element clientHeight

3. Element.offsetHeight

与 clientHeight 的区别是: offsetHeight 包含 borders 高度。

::before or ::after

参考 HTMLElement offsetHeight

4. Element.scrollHeight

元素全部内容(包含因溢出未呈现部分)的高度。度量方式和 Element.clientHeight 一样,包括 padding,但不包括 borders, margins, horizontal scrollbars。

如果元素全部内容都能够呈现在 box 中,不出现垂直滚动条,则 Element.scrollHeight 等于 Element.clientHeight。

Element scrollHeight

参考 Element scrollHeight

5. Element.scrollTop

表示元素内容向上滚动的高度值,即元素内容的顶部与呈现部分的顶部的距离,因此其值 >=0

如果元素没有滚动条(未溢出),即 scollTtop 为 0,如果元素内容滚动到最下端,则 scrollHeight = scrollTop + clientHeight。

参考 Element scrollTop

]]>
Ubuntu 系统上 Python 项目开发本地虚拟环境管理方案: pyenv + virtualenv 2020-05-13T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/python-environment-with-pyenv-virtualenv 1. 概述

由于使用 pipenv 安装相关包时非常慢,特别是 Lock 操作,故不推荐使用。

本文介绍用 Pyenv + virtualenv 管理 Python 项目开发的本地虚拟环境。

  • pyenv: 安装和管理多个 Python 版本。
  • virtualenv: 为每个项目创建独立的虚拟环境。

以下所有操作在 Ubuntu 16.04 系统上进行。

2. Python 版本管理: pyenv

2.1. 安装 pyenv

$ curl https://pyenv.run | bash

pyenv 相关的内容会安装在 ~/.pyenv/ 目录下。

安装后根据提示将以下内容添加到 ~/.bashrc:

export PYENV_ROOT="$HOME/.pyenv"
export PATH="~/.pyenv/bin:$PATH"
eval "$(pyenv init -)"

升级 pyenv:

$ pyenv update

删除 pyenv:

$ rm -rf ~/.pyenv

并删除 ~/.bashrc 中的相关环境变量。

2.2. 安装和管理多个 Python

查看可安装的版本:

$ pyenv install --list

安装指定版本:

$ pyenv install 3.8.2

安装 python 前,要先安装编译 python 所需的依赖包:

$ sudo apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev \
    libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev \
    xz-utils tk-dev libffi-dev liblzma-dev python-openssl git

Common build problems, 不然编译后导入某些 python 库时会出现 ModuleNotFoundError: No module named '_sqlite3' 等问题。

查看当前已安装的 python 版本:

$ pyenv versions
* system (set by /home/hy/.pyenv/version)
  3.8.2

通过 pyenv 安装的所有 Python 版本都保存在 ~/.pyenv/versions/ 目录下。

2.3. 每个目录可指定执行特定的 Python 版本

没有指定前,系统默认的 Python 为 2.7:

$ mkdir test
$ cd test
$ python
Python 2.7.12 (default, Oct  8 2019, 14:14:10) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> 

通过 pyenv local 命令指定,当在该目录下执行 python 时,执行的 python 版本:

$ pyenv local 3.8.2

$ ls -la
total 12
drwxrwxr-x  2 hy hy 4096 3月  10 16:04 .
drwxrwxr-x 42 hy hy 4096 3月  10 13:02 ..
-rw-rw-r--  1 hy hy    6 3月  10 16:03 .python-version

$ cat .python-version 
3.8.2

local 命令会在当前目录下生成一个包含版本号的隐藏文件 .python-version

验证执行的 python 版本:

$ python
Python 3.8.2 (default, Mar 10 2020, 13:47:49) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 

2.4. 切换全局 Python 版本

$ pyenv global 3.8.2

$ python
Python 3.8.2 (default, Mar 10 2020, 13:47:49) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 

3. 虚拟环境管理: pyenv-virtualenv

3.1. 安装 pyenv-virtualenv

$ git clone https://github.com/pyenv/pyenv-virtualenv.git ~/.pyenv/plugins/pyenv-virtualenv
$ echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bashrc
$ source ~/.bashrc

3.2. 创建独立的虚拟环境

创建项目目录:

$ pyenv virtualenv 3.8.2 py38

该命令为 python 3.8.2 创建一个名为 py38 的虚拟环境,保存在 ~/.pyenv/versions/ 下:

$ pyenv versions

  system
  *3.8.2
  3.8.2/envs/py38
  py38

切换和使用 python 虚拟环境:

$ pyenv activate py38
$ pip install django

$ pyenv deactivate

删除虚拟环境:

$ pyenv uninstall py38 # or
#$ rm -rf ~/.pyenv/versions/py38/

为切换虚拟环境命令设置 alias:

$ echo 'workon="pyenv activate "' >> ~/.bashrc
$ ~/.bashrc

$ workon py38

.bashrc 中的相关设置为:

# pyenv & virtualenv
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$HOME/.pyenv/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
alias workon='pyenv activate '

4. Mac 上的设置

和 Ubuntu 上的操作类似,但相关设置保存在 ~/.~/.bash_profile 中,不要放在 ~/.bashrc 中即可。

资源

]]>
thingsboard 规则引擎结点功能总结 2020-04-30T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/thingsboard-rule-engine-nodes 1. 概述

本文结合官方文档和v2.4.3版本源码总结了各规则引擎结点的功能。

官方文档的有些描述不太清楚,需要结合源码理清。

2. 核心概念

规则引擎是一个事件处理系统。

  • 能对由设备和资产上传的消息进行 filter, enrich, transform 处理
  • 并触发不同的动作,如 notification, 与外部系统交互等

2.1. Rule Engine Message 规则引擎消息

Rule Engine Message is a serializable, immutable data structure that represent various messages in the system. 例如:

  • 上传数据、属性更新、设备调用服务端 RPC
  • 实体生命周期事件: created, updated, deleted, assigned, unassigned, attributes updated;
  • 设备状态事件: connected, disconnected, active, inactive, etc;
  • 其它系统事件。

Rule Engine Message contains the following information:

  • Message ID: time based, universally unique identifier;
  • Originator of the message: Device, Asset or other Entity identifier;
  • Message Type: “Post telemetry” or “Inactivity Event”, etc;
  • Payload of the message: JSON body with actual message payload;
  • Metadata: List of key-value pairs with additional data about the message.

消息中包含以下信息:

  • Message ID: 基于时间的唯一 ID
  • 消息来源者:设备、资产(Asset) 、其它实体的 ID
  • 消息类型: Post telemetry, Inactivity Event
  • 消息体 Payload: JSON body.
  • Metadata: KV 键值对,消息的额外数据。

2.2. 预定义的消息类型

  • POST_ATTRIBUTES_REQUEST: Post attributes, 设备请求上传属性值,metadata 数据有: deviceName, deviceType, payload 例如 {"currentState": "IDLE"}
  • POST_TELEMETRY_REQUEST: Post telemetry, 设备请求上传遥测数据,metadata 数据有: deviceName, deviceType, ts(timestamp, 毫秒),payload 例如 {"temperature": 22}
  • TO_SERVER_RPC_REQUEST: RPC Request from Device, 设备(客户端)请求 RPC 调用,metadata 数据有: deviceName, deviceType, requestId(由客户端提供的 RPC 请求 ID),payload 例如 {"method": "getTime", "params": {"param1":"val1"}}
  • RPC_CALL_FROM_SERVER_TO_DEVICE: RPC Request to Device, 服务端请求 RPC 调用,metadata 数据有:requestUUID(服务端提供,用于区别应答),expirationTime, oneway(true 时无需应答,false 时需应用),payload 例如 {"method": "getGpioStatus", "params": {"param1": "val1"}}
  • ACTIVITY_EVENT: Activity Event, 设备切换为活跃状态,metadata 数据有 deviceName, deviceType
  • INACTIVITY_EVENT: Inactivity Event, 设备切换为非活跃状态。
  • CONNECT_EVENT: Connect Event, 设备已连接。
  • DISCONNECT_EVENT: Disconnect Event, 设备断开连接。
  • ENTITY_CREATED: Entity Created, 新实体已创建事件,metadata 数据有 userName(创建者名称), userId(创建者ID), payload 包含实体信息,如 {"id":{"entityType": "DEVICE", "id": "uuid"}, "createdTime": timestamp, ..., "name": "my-device", "type": "temp-sensor"}.
  • ENTITY_UPDATED.
  • ENTITY_DELETED.
  • ENTITY_ASSIGNED, Entity Assigned, 现有实体分配给客户事件,metadata 数据有 userName(操作者名称), userId, assignedCustomerName, assignedCustomerId.
  • ENTITY_UNASSIGNED.
  • ADDED_TO_ENTITY_GROUP.
  • REMOVED_FROM_ENTITY_GROUP.
  • ATTRIBUTES_UPDATED, Attributes Updated, 实体属性已更新事件,metadata 数据有 userName(操作者名称), userId, scope(SERVER_SCOPE 或 SHARED_SCOPE),payload 为已更新的属性键值对,如 {"softwareVersion": "1.2.3"}.
  • ATTRIBUTES_DELETED.
  • ALARM: Alarm Event, 当报警生成、更新、删除时产生该事件。
  • REST_API_REQUEST: REST API Request to Rule Engine,当用户执行 REST API 调用时产生该事件。

2.3. 规则结点 Rule Node

规则结点一次处理一个传入消息,并生成一个或多个输出消息。能过滤、增强、变换传入消息,执行动作或与外部系统交互。

2.4. 规则结点关联 Rule Node Relation

规则结点可关联到其它规则结点。每种关联都有关联类似 (Relation Type), 即表示该关联的逻辑意思的名称。规则结点在生成输出消息时,通过指定关联类型将生成的消息路由到下一个结点。

规则结点关联类型(即名称)可为 Success, Failure, 也可为 True, False, Post Telemetry, Attributes Updated, Entity Created 等。

2.5. 规则链

规则结点其及关联的逻辑组合。

租户管理员可定义一个根规则链(Root Rule Chain,默认规则链) 和多个其它规则链。

根规则链处理所有的输入消息,并可将消息转发到其它规则链。消息可在规则链间进行转发。

2.6. 规则结点类型

3. 体系结构

based on actor model and message queue:

规则引擎体系结构

4. 结点类型

4.1 Filter Node 过滤型结点

4.1.1. Check Relation Filter Node

消息发起者与当前结点中指定的实体关联性对比,比如结点中指定 Direction: From, Type: Asset, Asset: Field C, Relation type: Contains, 则当消息发起者是包含在 Field C 内的实体时,消息从 True 路流出,否则从 False 路流出。

4.1.2. Check Existence Fields Node

检测消息中的 data 和 metadata 中是否有相关字段。

4.1.3. Message Type Filter Node

检测消息类型。

4.1.4. Message Type Switch Node

根据消息类型路由到指定路径,未指定类型路径的消息路由到 Other 路径。

4.1.5. Originator Type Filter Node

检测输入消息中消息发起者的实体类型值,如果类型在结点中指定的类型中,则消息从 True 路流出,否则从 False 路流出。

4.1.6. Originator Type Switch Node

根据消息发起者的实体类型路由到指定路径。

4.1.7. Script Filter Node

结点中定义一个 bool 值 javascript 函数来过滤输入消息,函数有三个参数:

  • msg
  • metaData
  • msgType

例如:

function Filter(msg, metadata, msgType) {
    if(msgType === 'POST_TELEMETRY_REQUEST') {
        if(metadata.deviceType === 'vehicle') {
            return msg.humidity > 50;
        } else if(metadata.deviceType === 'controller') {
            return msg.temperature > 20 && msg.humidity > 60;
        }
    }

    return false;
}

4.1.8. Switch Node

结点中可定义一个返回字符串数组的 javascript 函数,同 Filter Node 也有三个参数,如:

if (msgType === 'POST_TELEMETRY_REQUEST') {
    if (msg.temperature < 18) {
        return ['Low Temperature Telemetry'];
    } else {
        return ['Normal Temperature Telemetry'];
    }
} else if (msgType === 'POST_ATTRIBUTES_REQUEST') {
    if (msg.currentState === 'IDLE') {
        return ['Idle State', 'Update State Attribute'];
    } else if (msg.currentState === 'RUNNING') {
        return ['Running State', 'Update State Attribute'];
    } else {
        return ['Unknown State'];
    }
}
return [];

返回值是关联路径名,基于路径名可关联到下一个 规则链

4.1.9. GPS Geofencing Filter Node

可定义2种地理围栏:

  • 多边形: 如果是定义在消息的 metadata 中,则定义为 {perimeter: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]],}。如果是定义在结点中,则 Perimeter typePolygon,定义体为 [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]]
  • 圆形:如果是定义在消息的 metadata 中,则需要定义这 4 个参数, centerLatitude, centerLongitude, range, rangeUnit,数值参数都是 double 浮点数,rangeUnit 表示单位,值为 METER, KILOMETER, FOOT, MILE, NAUTICAL_MILE。如果是定义在结点中,则 Perimeter typeCircle,并定义相关参数。

参见源文件 thingsboard/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java 中的 protected List<Perimeter> getPerimeters(TbMsg msg, JsonObject msgDataObj) throws TbNodeException

可以在结点中指定经纬度参数的 key 名,这些参数将在消息体 msg 或 metadata 中获取。

4.2. Enrichment Nodes 增强型结点

用于更新输入消息中的 metada。

4.2.1. Customer attributes 客户实体属性增强

找到输入消息发起者实体所属的客户实体,将该客户实体的属性值增加到消息的 metadata 中。

可配置源属性名和目标属性名。

如果选中 Latest Telemetry,则将客户实体中最新遥测数据中的值添加到消息的 metadata 中,否则添加客户的服务端属性(server scope)。

消息发起者实体类型必须为 Customer, User, Asset, Device,因为只有它们都有所属客户实体,如果不是,则路由到 Failure 路径。

4.2.2. Device attributes 设备实体属性增强

找到与输入消息发起者实体相关的设备实体,将该设备实体的所有属性值和最新遥测数据增加到消息的 metadata 中。

增加到 metadata 中的属性名前缀:

  • 设备的共享属性-> shared_
  • 设备的客户端属性 -> cs_
  • 设备的服务端属性 -> ss_
  • 设备的遥测数据 -> 无前缀

通过结点中的 Device relations query 配置进行相关设备实体查询。

Direction 值:

  • From: 表示该设备实体必须在关联关系的 From 端,而消息发起者在 To 端。
  • To: 与 From 相反。

Relation type: 指定关联类型。

Device types: 指定匹配设备的类型。

如果找个多处设备,则只用第一个找到的设备。没有找到时路由到 Failure 路径。

4.2.3. Originator attributes 消息发起者实体属性增强

类似 Device attributes。

4.2.4. Originator fields 消息发起者字段增强

将消息发起者实体的字段值添加到 metadata,结点中可以配置字段名映射信息。

消息发起者实体类型必须为 Tenant, Customer, User, Asset, Device, Alarm, Rule Chain,否则会路由到 Failure 路径。

是 Device attributes 结点的通用版本,通过 Relations query 定义关联实体查询方式。

4.2.6. Tenant attributes 租房实体属性增强

类似 Customer attributes 结点,将消息发起者所属租房的属性或最新遥测数据到 metadata。

4.2.7. Originator telemetry 消息发起者遥测数据增强

将消息发起者特定时间段内的遥测数据添加到 metadaba。

Latest timeseries 指定数据指定遥测数据键名,用 , 分隔多个键名。

Fetch mode:

  • FIRST: 从数据库中提取指定时间段最接近开始时间的数据。
  • LAST: 从数据库中提取指定时间段最接近结束时间的数据。
  • ALL: 提取时间段内的所有数据,以数组形式返回,可指定返回数据个数(最多1000)及排序。

时间段是根据当前系统时间点计算出来的(精确到毫秒),如当前系统时间是 ts, 结点中指定 Start Interval 为 2 Minutes, 指定 End Interval 为 1 Minutes,则时间段为 [ts-2601000, ts-1*60-1000]。

如果配置为 Use metadata interval patterns,则 Start IntervalEnd Interal 值可从消息的 metadata 中提取,从而只需在结点中定义要从 metadata 中提取的变量名模式,如 {metaKeyName}

详细见源码 thingsboard/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java 中的 private Interval getInterval(TbMsg msg) 函数。

4.2.8. Tenant details 租户实体数据库字段增强

将消息发起者所属租户的某些数据库字段增加到 metadata, 名称前加 tenant_ 前缀。

4.2.9. Customer details 客户实体数据库字段增强

类似 Tenant details

4.2.10. Transformation Nodes 变换结点

用于修改输入消息中的消息字段。

4.2.11. Change originator 变换消息发起者字段

输入消息中有一个 originator 字段用来表示消息的发起者实体。该结点能将 originator 值修改为消息发起者实体所属的客户或租户实体,或其关联的其它实体。查询关联实体要配置 Relation Query,具体和 related attributes 增强结点中的配置类似。

使用情景:若设备上传遥测数据,此时消息的发起者为该设备实体,通过修改为设备实体的租户实体,则之后该遥测数据则被归结到该租户。

4.2.12. Script Transformation Node 通过 javascript 脚本变换

结点中配置 function Transform(msg, metadata, msgType){} 函数,函数有三个参数,并返回变换后的消息,返回结构为:

{   
    msg: new payload,
    metadata: new metadata,
    msgType: new msgType 
}

4.2.13. To Email Node 变换为 Email 结点

将消息变换为 Email Message,Email 消息所需字段值可直接在结点中配置,若配置对应的变量模板,并从 metadata 中提取。转换后的消息可路由给 Send Email Node 结点。

4.3. Action Nodes 动作结点

基于输入消息执行各种动作。

4.3.1. 实体警报

警报信息保存在表 alarm 中,每个警报都有生命周期过程:创建/更新,清除,确认。

警报的发起者实体用字段 originator_idoriginator_type 表示。

一个实体的警报默认会传递到该实体关联的所有父实体上。

警报由发起者 originator, 警报类型 type, 和开始时间 start_ts 唯一标识,因此同一个实体,在同一时间,不能触发同类型的警报,只能对现有警报进行更新。

警报级别保存在字段 severity 中,值有: CRITICAL, MAJOR, MINOR, WARNING, INDETERMINATE.

警报的清除时间、确认时间和最近修改时间分别保存在字段 clear_ts, ack_ts, end_ts 中。

4.3.2. Create Alarm Node 创建/更新警报结点

结点会基于设备的 Alarm Type 和消息发起者实体,从数据库导入警报记录,若找到一个未清除的警报,则该结点会对该警报进行更新,否则在数据库中创建新警报。

Alarm Detial Builder 定义的javascript函数返回一个 Alarm Details JsonNode 对象,用于保存警报的一些额外信息,例如:

function Details(msg, metadata, msgType) {
    var details = {temperature: msg.temperature, count: 1};

    if (metadata.prevAlarmDetails) {
        var prevDetails = JSON.parse(metadata.prevAlarmDetails); // 注意 prevAlarmDetails 是 JSON 字串
        if(prevDetails.count) {
            details.count = prevDetails.count + 1;
        }
    }

    return details;
}

当前消息中的警报额外信息可从 metadata.prevAlarmDetails 中获取。

如果勾选了 Use message alarm data,则从消息的 payload 中导入警报配置信息,见源码 thingsboard/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java 中的 processAlarm 方法。

配置信息也可以配置为从消息的 metadata 中提取,如将 Alarm type 配置为 {metaKeyName}

如果勾选 Propagate,则还要配置警报要传递到父实体的关联类型,如 Contains, Managers, 用回车来添加。

创建/更新后的 Alarm 对象有如下属性:

  • Alarm details - object returned from Alarm Details Builder script, Details 函数返回的对象
  • Alarm status - 新创建的警报状态值为 ACTIVE_UNACK. 若是更新时,则状态值不变。
  • Severity - value from Node Configuration,警报等级
  • Propagation - value from Node Configuration,是否要上传给关联父实体
  • Alarm type - value from Node Configuration,警报类型
  • Alarm start time - 新创建时,开始时间是当前系统时间,若是更新时,则开始时间不变。
  • Alarm end time - current system time,表示最近修改时间,为当前系统时间。

结点处理后产生的消息结构为:

  • Message Type - ALARM
  • Originator - the same originator from inbound Message,同传入消息的
  • Payload - JSON representation of new Alarm that was created/updated,上节描述的 Alarm 对象数据
  • Metadata - all fields from original Message Metadata,来自原传入消息的 metadata。

若是新创建,则 metadata 会有 isNewAlarm: true,并且消息将路由到 Created 路径。 若是更新,则 metadata 会有 isExistingAlarm: true,并且消息将路由到 Updated 路径。

消息体 Payload 例如如下:

{
  "tenantId": {
    "entityType": "TENANT",
    "id": "22cd8888-5dac-11e8-bbab-ad47060c9bbb"
  },
  "type": "High Temperature Alarm",
  "originator": {
    "entityType": "DEVICE",
    "id": "11cd8777-5dac-11e8-bbab-ad55560c9ccc"
  },
  "severity": "CRITICAL",
  "status": "ACTIVE_UNACK",
  "startTs": 1526985698000,
  "endTs": 1526985698000,
  "ackTs": 0,
  "clearTs": 0,
  "details": {
    "temperature": 70,
    "ts": 1526985696000
  },
  "propagate": true,
  "id": "33cd8999-5dac-11e8-bbab-ad47060c9431",
  "createdTime": 1526985698000,
  "name": "High Temperature Alarm"
}

4.3.3. Clear Alarm Node 清除警报结点

基于结点中的 Alarm type 和输入消息的发起者实体,从数据库中加载相关警报并进行清除操作。

结点中定义的 Alarm Details Builder javascript 函数用来更新 Alarm Details 对象数据。

结点对警报进行如下更新:

  • 若警报当前状态为 ACK,则更新为 CLEARED_ACK, 当前为 UNACK, 则更新为 CLEARED_UNACK。
  • 设置 clear_ts 为当前系统时间。
  • Alarm Details Builder 返回值更新 Details 数据。

如果相关警报未找到,或已清除,路由到 False 路径。否则路由到 Cleared 路径。

该结点更新后的消息中 metadata 中会添加 isClearedAlarm:true

4.3.4. Delay Node 延时结点

将消息延时一段时间后再转出。结点中可配置延时多长时间,及最大待转队列长度。队列满后,传入的消息会路由到 Failure 路径。

4.3.5. Generator Node 消息生成器结点

结点中可配置生成消息的个数,时间间隔,消息发起者,并定义一个返回消息体的 javascript 函数,例如:

function Generate(prevMsg, prevMetadata, prevMsgType) {
    return {   
        msg: new payload,
        metadata: new metadata,
        msgType: new msgType 
    }
}

4.3.6. Log Node 日志结点

定义一个 javascript 函数,例如:

function ToString(msg, metadata, msgType) {
    return 'Incoming message:\n' + JSON.stringify(msg) + '\nIncoming metadata:\n' + JSON.stringify(metadata);
}

将消息转换成一个字串,并写入日志。写入的 LOG Level 是 INFO

4.3.7. RPC 功能

Thingsboard 中可以在服务端应用中调用设备实体应用中的 RPC,也可以从设备实体应用中调用服务端应用中的 RPC。

服务端发起的 RPC 调用为服务端 RPC,分 2 种:

  • One-way RPC request: 单向 RPC 请求无需设备进行收到确认或应答。
  • Two-way RPC request: 双向的则需要设备在指定时间内有应答。

服务端通过 System RPC Service 提供的功能向设备发送 RPC 调用,即向 http(s)://host:port/api/plugins/rpc/{callType}/{deviceId} POST RPC 请求,其中:

  • callType: 值为 oneway 或 twoway
  • deviceId:设备 ID

请求体是包含 methodparams 字段的 JSON 对象,如:

{
  "method": "setGpio",
  "params": {
    "pin": "23",
    "value": 1
  }
}

设备端可以使用 MQTT RPC API, CoAP RPC API, HTTP 进行 RPC 调用。

4.3.8. RPC Call Reply Node,RPC 调用应答结点

该结点向 RPC 调用者,即消息发起者 originator 发送 RPC 应答。由于 RPC 调用者必须是设备实体,因此消息发起者也必须是设备实体。

设备端发起的 RPC 请求会作为消息流转过各规则链进行处理,消息 metadata 中都有 request ID 相关的字段,用来匹配请求和应答消息。

结点中可配置 metadata 中 request ID 字段映射名,默认为 requestId

结点基于 requestId, originator, 输入消息中的数据,创建应答包向设备发送应答。

有以下情况,结点处理后的消息会路由到 Failure 路径:

  • 输入消息的发起者不是设备实体
  • 消息 metadata 中没有 request ID 数据
  • 消息数据为空

4.3.9. RPC Call Request Node,向设备发送 RPC 请求结点

向设备(即该消息的发起者)发送 RPC 请求,等待应答,并将应答作为输出消息,路由到下一个结点。

结点中可配置等待超时时间 timeout。

消息数据中必须要有 methodparams 字段,如:

{
  "method": "setGpio",
  "params": {
    "pin": "23",
    "value": 1
  }
}

如果消息数据中没有 requestId 字段,系统会自动创建一个随机数填入。

生成的消息内容为:

  • 发起者 originator: 同传入消息
  • metadata: 同传入消息
  • 消息数据:设备的 RPC 应答内容会添加到消息数据中。

有以下情况,输出的消息会路由到 Failure 路径:

  • 输入消息的发起者不是设备实体
  • 输入消息数据中没有 methodparams 字段
  • 结点获取 RPC 应答超时

4.3.10. Save Attributes Node 存储实体属性结点

将输入消息数据中的各值作为消息发起者实体的属性值存入数据库。结点中可配置对应存入的属性域,如客户端属性、共享属性、服务端属性。

输入消息类型必须为 POST_ATTRIBUTES_REQUEST,否则输出消息会路由到 Failure 路径。消息体例如:

{
  "firmware_version": "1.0.1",
  "serial_number": "SN-001"
}

其它消息类型可前置一个 Script Transform Node 来进行消息类型转换。

4.3.11. Save Timeseries Node 存储实体的时序数据结点

将输入消息数据中的时序遥测数据作为消息发起者实体的时序数据存入数据库。结点中可设置默认的 TTL 数值为时序数据过期秒数,TTL 只当使用 Cassandra 存储时有用。

输入消息类型必须为 POST_TELEMETRY_REQUESTS,否则输出消息会路由到 Failure 路径。消息体例如:

{  
  "values": {
    "key1": "value1",
    "key2": "value2"
  }
}

其它消息类型可前置一个 Script Transform Node 来进行消息类型转换。

消息的 metadata 中必须有 ts 字段用来指定上传的遥测数据的时间,如果 metadata 有 TTL 字段,则结点优先使用该值。

4.3.12. Save to Custom Table 将消息内容存储到 Cassandra 自定义表的结点

只适用于 Cassandra 数据库

将消息数据中的各字段存储到 Cassandra 某些的对应列中,结点中需配置表名,及字段名和列名的对应关系。

4.3.13. Assign To Customer Node 将消息发起者分配给某客户实体的结点

输入消息的发起者实体类型必须为:Asset, Device, Entity View, Dashboard。

结点中配置目标客户的名称,该值也可设置为名称变量模式如 {metaKeyName} 从而从消息的 metadata 中提取。

该值与数据库表 customer 中字段 title 进行匹配从而提取出相应客户实体,若结点中勾选 Create new customer if not exists,则在未找到时新建一个。

见源码 thingsboard/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java 中的 protected ListenableFuture<CustomerId> getCustomer(TbContext ctx, TbMsg msg) 方法。

4.3.14. Unassign From Customer Node 将消息发起者从目标客户实体处取消分配的结点

Assign To Customer Node 功能相反。

4.3.15. Create Relation Node 创建关联关系的结点

为所选的目标实体与消息发起者实体创建关联关系。消息发起者必须为如下实体: Asset, Device, Entity View, Customer, Tenant, Dashboard.

结点中要配置目标实体的查询方法,如:

  • 实体类型 Type
  • 实体名 Name pattern 可设置为静态时,也可设备为变量模式如 {metaKeyName} 从而从消息的 metadata 中提取。
  • 类别名 Type pattern 可设置为静态时,也可设备为变量模式如 {metaKeyName} 从而从消息的 metadata 中提取。

见源码 thingsboard/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java 中的 private EntityContainer loadEntity(EntityKey entitykey) 方法,查询是 Name pattern 的值一般是匹配数据库实体记录中的 nametitle 字段。

勾选 Create new entity if not exists 则在找不到实体时会创建新实体。

需创建的关联关系还要配置:

  • Direction: From, To。例如当值为 From 时,则表示目标实体应当在关联关系的 From 端,而消息发起者在 To 端。见源码 thingsboard/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java 中的 protected SearchDirectionIds processSingleSearchDirection(TbMsg msg, EntityContainer entityContainer) 方法。
  • Relation Type:如 Contains, Managers

勾选 Remove current relations 会删除旧有的相同关联。 勾选 Change originator to related entity 会将输出消息中的发起者更改为找到的目标实体。

4.3.16. Delete Relation Node 删除关联关系的结点

类似 Create Relation Node

4.3.17. GPS Geofencing Events Node

从输入消息中提出坐标,与地理围栏对比,生成相应的事件:

  • Entered: 进入围栏
  • Left: 离开围栏
  • Inside: 当前状态在围栏内
  • Outside: 当前状态在围栏外

Minimal inside durationMinimal outside duration 表示消息发起者实体处于相关状态的持续时间。

结点参数配置类似 GPS Geofencing Filter Node

4.4. 外部结点

实现与外部系统交互。

结点的输出消息的 metadata 中会包含外部系统应答消息中的一个关键字段,原输入消息的数据原样复制到输出消息中。

4.4.1. AWS SNS Node

发布消息到 Amazon Simple Notification Service。

4.4.2. AWS SQS Node

发布消息到 Amazon Simple Queue Service。

4.4.3. Kafka Node

发送消息到 Kafka 代理。

4.4.4. MQTT Node

将输入消息的数据发布到相应的 MQTT 主题上。

主题可基于 metadata 中的字段创建。结点中需配置 MQTT 代理的连接参数。

4.4.5. RabbitMQ Node

将输入消息的数据发布到 RabbitMQ.

4.4.6. REST API Call Node

调用外部系统的 REST API。

  • Endpoint URL 和 URL 头信息可基于 metadata 中的字段创建。
  • 消息数据作为 REST 的请求体发送。

输出消息的 metadata 中会包含 REST 应答中的 status, statusCode, statusReason, headers。输出消息体 message payload 与 REST 应答体相同。消息类型和发起者不变。

4.4.7. Send Email Node

发送邮件结点,其前置结点必须是 To Email 结点。

结点中需配置邮件服务器连接方式。

4.4.8 Twilio SMS Node

通过 Twilio 发送短信。

Resources

  • https://thingsboard.io/docs/user-guide/rule-engine-2-0/overview/
  • https://thingsboard.io/docs/user-guide/rule-engine-2-0/architecture/
  • https://thingsboard.io/docs/user-guide/rule-engine-2-0/filter-nodes/
  • https://thingsboard.io/docs/user-guide/rule-engine-2-0/enrichment-nodes/
  • https://thingsboard.io/docs/user-guide/rule-engine-2-0/transformation-nodes/
  • https://thingsboard.io/docs/user-guide/alarms/
  • https://thingsboard.io/docs/user-guide/rule-engine-2-0/action-nodes/
  • https://thingsboard.io/docs/user-guide/rule-engine-2-0/external-nodes/
]]>
thingsboard 实体及数据存储研究 2020-04-27T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/thingsboard-entity-database 1. 概述

本文主要研究 thingsboard 各种实体在关系型数据库 postgres 中的存储。

2. 安装与配置使用 Postgresql 数据库

从源码编译安装参考 : https://thingsboard.io/docs/user-guide/contribution/how-to-contribute/

编译运行后,需创建 thingsboard 数据库:

psql -U postgres -d postgres -h 127.0.0.1 -W
CREATE DATABASE thingsboard;
\q

Postgresql 的基本使用,见 http://www.atjiang.com/postgresql-beginner-11-tasks/

Thingsboard v2.4 中,默认使用的数据库用户名和密码都是 postgres, 见 /application/src/main/resources/thingsboard.yml:

datasource:
    driverClassName: "${SPRING_DRIVER_CLASS_NAME:org.postgresql.Driver}"
    url: "${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thingsboard}"
    username: "${SPRING_DATASOURCE_USERNAME:postgres}"
    password: "${SPRING_DATASOURCE_PASSWORD:postgres}"

创建 schema 并导入测试数据:

cd ~/workspace/thingsboard/application/target/bin/install
chmod +x install_dev_db.sh
./install_dev_db.sh

登录测试

http://127.0.0.1:8080/

用户名 tenant@thingsboard.org 密码 tenant

实体

Tenants 租户

是一个独立的商业实体:比如个人或组织,可以拥有若产生多个设备或资产。一个租户可有多个租户管理员用户及多个客户。

可将它理解为代理商,中间商,如中国移动是一个 tenant, 承包了某地区的所有项目,信息保存在 tenant 表中。

Customers 客户

也是一个独立的商业实体:如何个人或组织,客户从租户那购买设备和资产。一个客户中可有多个用户。

可将它理解为甲方客户,如某单位或企业,从租户中国移动那购买服务。

信息保存在 customer 表中, 通过 tenant_id 关联相应的 tenant。

Users 用户

用户登录系统,查看信息并管理实体的账号。系统中创建的用户,保存于 tb_user 中。

通过 customer_idtenant_id 关联相应的租户和客户,用户权限由 authority 字段指定,值见:

// thingsboard/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java

public enum Authority {
    
    SYS_ADMIN(0),
    TENANT_ADMIN(1),
    CUSTOMER_USER(2),
    REFRESH_TOKEN(10);
}

customer_id 和 tenant_id 有一个默认的值 1b21dd2138140008080808080808080,该值应该为系统中的一个默认 Tenant 和 Customer 值。

例如系统管理员账户 “sysadmin@thingsboard.org” 对应的 customer_id 和 tenant_id 即为该值。

Devices 设备

设备可上传遥测数据或执行 RPC 命令。

保存于 device 表中,通过 customer_idtenant_id 关联相应的租户和客户。设备类型由 type 字符串字段指定,默认为 default, 可自由创建新值。

新建的设备,其默认 customer_id 为 1b21dd2138140008080808080808080,该值应该为系统中的一个默认 Customer 值。

通过设备管理界面可以分配给指定客户。

设备的访问凭证信息保存在表 device_credentials 中,类型由 credentials_type 字段指定,类型值见:

// thingsboard/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java
public enum DeviceCredentialsType {
    ACCESS_TOKEN,
    X509_CERTIFICATE
}

Assets 资产

一种用于关联其它设备和资产的抽象实体,如区域、建筑物、单位等,是一个容器概念。 保存于表 asset 中。

Alarms 警报

与各种实体关联的警报事件。

保存于表 alarm,由 originator_idoriginator_type 关联实体,状态由 status 字段指定,值见:

// thingsboard/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSearchStatus.java
public enum AlarmSearchStatus {
    ANY, ACTIVE, CLEARED, ACK, UNACK
}

Dashboards

可视化面板,可可视化数据,并操控设备。

保存于表 dashboard 中,Dashboard 由 tenant 所有,通过 tenant_id 字段关联。并可以分配给多人 customer, 分配信息保存在 assigned_customers 字段中,如:

[
    {
        "customerId":{
            "entityType":"CUSTOMER",
            "id":"5c3c4ff0-8763-11ea-b734-6151c7bf4d3f"
        },
        "title":"Customer B",
        "public":false
    },
    {
        "customerId":{
            "entityType":"CUSTOMER",
            "id":"5c360e60-8763-11ea-b734-6151c7bf4d3f"
        },
        "title":"Customer A",
        "public":false
    }
]

Rule Node 规则结点

处理结点,对上报的信息及实体事件进行处理。

保存在表 rule_node 中,通过 rule_chain_id 关联到某个规则链。

系统已创建了多种结点类型,当前结点类型在 type 字段中指定,值例如(有很多):

  • “org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode”
  • “org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode”
  • “org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode”
  • “org.thingsboard.rule.engine.action.TbLogNode”
  • “org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode”

见 thingsboard/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java, thingsboard/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java 等文件中的定义。

配置信息保存在字段 configuration 中,是 JSON 字串,如 {"timeoutInSeconds":60}

additional_info 字段中保存当前结点在规则链配置界面中的坐标,是 JSON 字串,如 {"layoutX":824,"layoutY":138}

Rule Chain 规则链

关联的多个规则结点的一个逻辑单元。

保存于表 rule_chain 中,首个结点通过 first_rule_node_id 指定,通过 root boolean 指定是否为根链,通过 tenant_id 关联 Tenant。描述信息等以 JSON 字串形式保存在 additional_info 字段中。

所有实体支持属性、遥测数据和关联: Attributes, Telemetry date, Relations.

Attributes 属性

属性值是键值对, 保存于表 attribute_kv 中。实体由 entity_typeentity_id 字段关联,属性类型由 attribute_type 指定,值见:

// thingsboard/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
public class DataConstants {
    public static final String CLIENT_SCOPE = "CLIENT_SCOPE";
    public static final String SERVER_SCOPE = "SERVER_SCOPE";
    public static final String SHARED_SCOPE = "SHARED_SCOPE";

    public static final String[] allScopes() {
        return new String[]{CLIENT_SCOPE, SHARED_SCOPE, SERVER_SCOPE};
    }

kv 值中的 key 在 attribute_key 中保存,value 在 bool_v, str_v, long_v, dbl_v 字段其中一个中保存,分别表示为 bool, string, long, double 值。

Telemetry data 遥测数据

时序数据。

历史值在 ts_kv 表中保存, 最新数据在 ts_kv_latest 表中保存。

实体由 entity_typeentity_id 字段关联,key 在 key 字段,时间在 ts 字段,值在 bool_v, str_v, long_v, dbl_v

Relations 关联

实体间的有向连接关系信息。

保存于表 relation 中,关联是两个实体间的一种关系,由 from_id, from_type 字段指定 from 端的实体,实体类型值见:

// thingsboard/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
public enum EntityType {
    TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE
}

to_id, to_type 字段指定 to 端的实体。

关联关系由 relation_type_grouprelation_type 字段指定。

Audit Logs 审计日志

审计/操作记录信息保存在表 audit_log 中,操作者通过 tenant_id, customer_id, user_id, user_name 字段指定,操作对象通过 entity_id, entity_type, entity_name 字段指定, 操作类型由 action_type 字段指定,值见:

// thingsboard/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
public enum ActionType {
    ADDED(false), // log entity
    DELETED(false), // log string id
    UPDATED(false), // log entity
    ATTRIBUTES_UPDATED(false), // log attributes/values
    ATTRIBUTES_DELETED(false), // log attributes
    TIMESERIES_DELETED(false), // log timeseries
    RPC_CALL(false), // log method and params
    CREDENTIALS_UPDATED(false), // log new credentials
    ASSIGNED_TO_CUSTOMER(false), // log customer name
    UNASSIGNED_FROM_CUSTOMER(false), // log customer name
    ACTIVATED(false), // log string id
    SUSPENDED(false), // log string id
    CREDENTIALS_READ(true), // log device id
    ATTRIBUTES_READ(true), // log attributes
    RELATION_ADD_OR_UPDATE(false),
    RELATION_DELETED(false),
    RELATIONS_DELETED(false),
    ALARM_ACK(false),
    ALARM_CLEAR(false),
    LOGIN(false),
    LOGOUT(false),
    LOCKOUT(false);

    private final boolean isRead;

    ActionType(boolean isRead) {
        this.isRead = isRead;
    }
}

操作数据以 JSON 字串形式保存在 action_data 中。

操作状态在 action_status 字段指定,值有 SUCCESSFAILURE,如果失败,则失败信息在 action_failure_details 字段中保存,如 PRC Error: Timeout

资源

]]>
C 系程序员20分钟学会 Dart 语言 2020-04-09T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/dart-intro-for-c-style-programmers 1. 概述

Dart 是 Google 下一代操作系统 Fuchsia 的御用程序开发语言,而是 App 跨平台框架 Flutter 使用的开发语言。它是一种面向对象的语言,使用 C 风格语法,揉合了 Javascript、Python、Java 等语言的相关特性。

如果之前熟悉这几门语言,可以快速入门。

2. 应知就会

2.1. 语句结束符

同 C 语言一样,Dart 所有语句都以 ; 结束。

2.2. 注释

同 Javascript,单行用 //, 多行用 /* */

如果要支持文档生成工具,则单行注释用 ///,多行注释用 /** */, 例如:

/// This is a documentation comment

/**
  This , too,
  is a
  documentation comment.
*/

2.3. 变量声明与类型

Dart 是强类型语言,即所有变量都是有类型的。

同 C 语言一样,可以在声明变量时指定类型 <specific_type> variable; ,如:

int a = 3;

也可以同 Javascript 中一样,用 var a = 3; 声明,此时 Dart 会从赋值语句的右值推导出变量的类型,如本例中将推导出 a 变量是 int 型。 那么之后该变量的类型就确定了,不能再赋值其它类型的值了,例如本例中再给 a 赋值字符串值编译会通不过: a = "String Value";

如果将变量声明为 dynamic 类型,那么当变量赋值为一个类型的值后,可随时再次赋值为其它类型的值,如:

dynamic x = 42;
x = "Hello World";

同 Java 一样, Dart 中的一切都是对象,都最终继承自 Object 类,因此将变量声明为 Object 类型类似于声明为 dynamic 类型,可以赋值任何类型的值,如:

Object x = 42;
x = "Hello World";

两都区别是: dynamic 类型告诉 Dart 不要查检变量的类型,即关闭类型检测功能,因此若引用 dynamic 变量上不存在的方法时,编译是能通过的,但在运行时出错; 而引用 Object 类上不存在的方法时,编译就不能通过。

2.4. 常量和 final 值

同 Java 类似,常量值用 const 修饰,常量值在编译时就被确定,不能修改,如:

const x = "Hello";
const String y = "world";

final 变量可以在运行时赋值,但只能赋值一次,例如想确定程序启动时间值,可以声明为一个 final 变量,如:

final x = DateTime.now();

const 不仅能修饰变量,也可以修饰值,如:

List lst = const [1, 2, 3];
lst[0] = 999; // compile error

2.5. 类型

基本类似用小写开头,如 int, double, num, bool, 其中 numint, double 的父类。其它类型以大写开头,如 String, List, Map

2.5.1. 数字型

同 C 一样,int, double, num 都支持 +, -, *, /, %。数字型唯一特殊的操作符是 ~/,表示返回除法结果的整数部分,功能同 Python 中的 // 操作符,如:

int x = 3;
double y = 2.0;
x = x ~/ y;
print(x); // 1

同 C 中一样,dart 中 x = x + y 也可以缩写为 x += y,这种缩写同样适用于 -, *, /, %~/ 操作符,如 x = x ~/ y 可写为 x ~/= y

也有 C 中类似的 v++, ++v, v--, --v 等前后缀操作符及三元条件表达式如: x = a ? b : c

Dart 中还支持一种特有的二元条件表达式,如 x = a ?? b;,表示当 a 有值时(即不为 null 值,声明的变量未初始化时值默认为 null),x 赋值为 a,否则赋值为 b。

2.5.2. 字符串型 String

同 Javascript 中类似,字符串即可以用单引号 'xxx',也可以用双引号 "xxx" 表示。字符串中若包含 ${val} 形式的变量引用,则生成的字符串结果中会自动用变量值进行替换,这是一种很方便的功能,如:

String name = "Haiiiiiyun";
String greeting = "Hello ${name}"; // Hello Haiiiiiyun
String greeting = "Hello $name"; // 也可省略 {},直接用 $var

字符串与数字间可进行相互转换,如:

int i = 42;
double d = 4.2;
String si = i.toString(); // "42"
String sd = d.toString(); // "4.2";

int i2 = int.parse(si); // 42
double d2 = double.parse(sd); // 4.2

2.5.3. 布尔型 bool

只有 truefalse 两个值,注意,在 if while 等条件判断表达式的值不能隐匿转换成 bool 类型值,这和一般的语言都不同,例如:

if (1) { // 
    print("true");
}

不能编译通过,因为 1 不能转换成 bool 型值 true

2.5.4. 枚举型 Enum

类型 C,可用 enum 定义枚举类型,如: enum Week{ Mon, Tue, Wed, Thu, Fri, Sat, Sun };

其每个值都有一个 index 值,如 Week.Mon.index 的值为 0Week.Tue.index 值为 1,以此类推。枚举值适用于 switch 语句。

2.5.5. List

List 类似 Javascript 中的数组,它是有序的,因此有 indexOf 方法,操作有:

List lst = ['a', 'b', 'c'];
lst.add('d'); // ['a', 'b', 'c', 'd']
lst.removeLast(); // ['a', 'b', 'c']
print(lst.indexOf('a')); // 0

Set 和 Python 中的 Set 类似,是无序数组,且其中的元素不会重复:

Set chars = Set();
chars.addAll([ "a", "b", "c" ]);
chars.add("a"); // 还是 ["a", "b", "c"]
chars.remove("b"); // ["a", "c"]
chars.contains("a")); // true
chars.containsAll([ "a", "b" ])); // false

2.5.6. Map

类似 Javascript 中的 dict,操作有:

Map<String, String> countries = Map();
countries['India'] = "Asia";
countries["Germany"] = "Asia"; // wrong
countries["France"] = "Europe";
countries["Brazil"] = "South America";

if (countries.containsKey("Germany")) {
    countries["Germany"] = "Europe"; // update
    print(countries); // {India: Asia, Germany: Europe, France: Europe, Brazil: South America}
}

countries.remove("Germany");
print(countries); // {India: Asia, France: Europe, Brazil: South America}

2.6. 类型测试和类型转换

is 关键字进行类型测试,用 as 关键字进行类型转换,如

// Pig 是 Animal 的子类

if(animal is Pig) {
    (animal as Pig).oink(); //叫声
}

2.7. 流程控制

if, else 和 C 中完全一样。条件表达式也可用 ||, &&! 进行组合。

switch 也和 C 中一样。

2.8. 循环

whiledo while 循环和 C 中完全一样。同时 continuebreak 语句也一样使用。

2.8.1 for 循环

第一个 for 循环形式和 C 类型,如:

for (var i=0; i<10; i++){
    print(i);
}

第二个 for-in 形式适用于可迭代对象,如 List, Iterator 等:

List lst = ['a', 'b', 'c'];
for (var char in lst) {
    print(char);
}

2.9 异常处理

异常处理和 Java 类似。基类是 ErrorException,若要捕获某种类型的异常,用 on <SpecificException> catch(e), 若不关心捕获的异常类型,直接用 catch(e),而 finally 段中的代码不管有没有异常捕获到最会最后执行,如:

try {
    somethingThatMightThrowAnException();
} on FormatException catch (fe) {
    print(fe);
} on Exception catch (e) {
    Print("Some other Exception: " + e);
} catch (u) {
    print("Unknown exception");
} finally {
    print("All done!");
}

3. 函数与对象

Dart 中所有变量都是对象,因此函数也是对象,其类型是 Function

函数定义同 C 中类似,如:

int add(int a, int b)
{
    return a + b;
}

即定义函数时要指定函数返回值类型,及参数的类型。如果没有指定返回值类型,默认为 void。

类似 C, main(List<String> args) 函数是程序的入口函数。

3.1 命名函数参数

普通函数调用时,其参数值由其位置确定,如上面的函数调用:

int x = add(1, 2); // 3

命名函数参数在 {} 中指定,如:

int add2(int init, {int a, int b}){
    return init + a + b;
}

print(add2(1, b:2, a:1)); // 4

其中的命名参数值在调用时用形如 x:y 形式指定,并且参数位置与定义时的位置无关,但必须在普通位置函数值之后。

3.2. 参数默认值

可以指定命名参数的默认值,如:

int add3({int init=0, int a, int b}){
    return init+a+b;
}

print(add3(a:1, b:2)); // 3
print(add3(init:10, a:1, b:2)); // 13

3.3. 可选参数

将参数放在 [] 指定其为可选参数,在调用时可不用提供参数值,如:

String add4(int a, int b, [String c]){
    return "$c: ${a+b}";
}

main(){
    print(add(1,2));
    print(add2(1, a:1,b:2));
    print(add3(a:1, b:2));
    print(add3(init:10, a:1, b:2));

    print(add4(1,2)); // null: 3
    print(add4(1,2, "Sum")); // Sum: 3
}

3.4. 匿名函数

类似 Javascript 和 Java 中的匿名函数,创建的匿名函数可以赋值给 Function 的变量,之后该变量和普通的函数一样调用,如:

Function add5 = (int a, int b) {
    return a+b;
};

print(add5(1,2)); // 3

如果函数体如本例一样,只是一条 return 语句,则可以简写为 fat arrow 形式(和 Python lambda 函数类似):

Function add6 = (int a, int b) => a+b;
print(add6(1,2)); // 3

3.5. 高阶函数

函数是对象,可以作为函数参数值传递。而能接受函数值作为参数的函数即为高阶函数。

例如:

Function add6 = (int a, int b) => a+b;

int operator(Function op_fun, int a, int b)
{
    return op_fun(a, b);
}

print(operator(add6, 1, 2)); //3

这里的 operator 函数即为高阶函数。

3.6. 闭包 Closure

闭包是一个特殊的函数,也叫闭包函数。其特点时:当定义闭包函数时,闭包函数将定义函数时其父作用域中的变量值都固定下来,从而当调用闭包函数时,使用的也是定义时的变量值。

这在函数里定义闭包函数时特别明显(函数体中可以定义函数),如:


Function adder(int step)
{
    return (a) => a + step;
}

var adder1 = adder(1);
print(adder1(1)); // 2

var adder10 = adder(10);
print(adder10(1)); //11

闭包函数 adder1 将 step 值固定为定义时的 1,而 adder10 将 step 值固定为 10

4. 类与面向对象

同 Java 类似,类用 class 定义,如:

class Animal {
    int numLegs = 0;
    int numEyes = 0;

    Animal(int numLegs, int numEyes){
        this.numLegs = numLegs;
        this.numEyes = numEyes;
    }

    void eat() {
        print("Animals eat everything depending on what type it is.");
    }
}

var a1 = new Animal(4, 2);
var a2 = Animal(4, 2); // new 关键字可省略

创建类实例也用 new ClassName(arg),其中的 new 关键字可省略,这样创建类实例和调用函数在形式上就一样了。

4.1 属性和方法

其中的 numLegsnumEyes 是属性,可以直接访问实例中的属性值,如:

var a1 = new Animal(4, 2);
a1.numLegs = 2;
print(a1.numLegs); //2

其中的 eat() 是方法,可以和函数调用一样调用类实例上的方法。

访问属性和方法时,如果对象为空,会抛出异常,类似 Typescript, Dart 中的 ?. 操作符用来访问对象属性时,如果对象为 null,则不访问, 从而避免了抛出异常:

var a1 = Animal(4, 2);
a1 = null;
print(a1?.numLegs); //不会抛出异常。

静态属性和静态方法都用 static 修改,它们是类级别的属性和方法,可以直接在类上访问:

class Circle {
    static const pi = 3.14;
    static void drawCicle() {
        //...
    }
}

print(Circle.pi); // 3.14
Circle.drawCicle();

4.2. 构造方法

类中和类名相同的方法是构造方法,构造方法可以有参数,例如本例中的构造方法,其功能只是设置类的各属性值,像这种构造方法在 Dart 中可以简写为:

class Animal {
    int numLegs;
    int numEyes;

    Animal(int this.numLegs, int this.numEyes);

    void eat() {
        print("Animals eat everything depending on what type it is.");
    }
}

可定义多个构造方法,方法名要么和类名 <ClassName> 相同,要么以 <ClassName>. 为前缀,例如:

class Animal {
    int numLegs = 0;
    int numEyes = 0;

    Animal(int this.numLegs, int this.numEyes); //构造方法

    Animal.namedConstructor(int this.numLegs, int this.numEyes) { //另一个构造方法
    }

    void eat() {
        print("Animals eat everything depending on what type it is.");
    }
}

4.2.1. 默认构造方法

和 C++ 类似,如果没有声明构造方法,编译器会自动生成一个无参数的默认构造方法,该默认构造方法也会调用父类的无参构造方法。

4.2.2. 构造方法不能继承

子类不能继承父类的构造方法。子类的构造方法通过 super 调用父类的构造方法,并且和 C++ 中类似,将这种父类调用放在初始化列表中,如:

class Cat extends Animal {
    Cat(): super(4,2) {
    }
}

初始化列表中的代码是最先执行的。

4.2.3. 重定向构造方法

一个构造方法通过初始化列表,只调用另一个构造方法,没有方法体,如:

class Animal {
    int numLegs = 0;
    int numEyes = 0;

    Animal(int this.numLegs, int this.numEyes); //构造方法

    Animal.cat(): this(4, 2); // 重定向构造方法

    void eat() {
        print("Animals eat everything depending on what type it is.");
    }
}

4.2.4. getter 和 setter

import 'dart:math';

class Square {
    double width;

    
    Square(this.width);

    double get area => width * width;

    set area(inArea) => width = sqrt(inArea);
}

var s = Square(5);
print(s.area); // 25.0

s.area = 16;
print(s.width); // 4.0

get 关键字设置 getter, 用 set 关键字设置 setter。

4.3. 子类

和 Java 一样,生成子类的语法是 class SubClass extends SuperClass {}。 Dart 不支持多重继承,只能 extends 自一个父类。

4.4. 抽象类

类似 Java, 抽象类用 abstract 修饰,抽象类中可以只声明方法,也可以有默认的实现:

abstract class Shape {
    double area(); //只有声明

    void draw(){ //有默认实现体
        print("draw here");
    }
}

抽象类不能实例化。

4.4. 接口 interface

Dart 中类与接口不分,普通类和抽象类都是接口。类不能多重继承,但可以实现多个接口,用 implements 关键字,多个接口用 , 分隔,如:

abstract class Shape {
    double area(); //只有声明

    void draw(){ //有默认实现体
        print("draw here");
    }
}

class Circle implements Shape {

    @override
    double area(){
    }

    @override
    void draw(){ //有默认实现体
        print("draw here");
    }
}

如果用 implements 实现接口,则必须实现(重写)接口中的全部方法和属性,不管接口中有没有默认实现。

4.6. mixin

mixin 是实现代码复用的一种方式。

Dart 中类与mixin不分,普通类和抽象类都是mixin。类不能多重继承,但可以引入多个mixin,用 with 关键字,多个mixin用 , 分隔,如:

class Shape {
    double area() {
    }

    void draw(){
        print("draw here");
    }
}

class Circle with Shape {
}

var c = Circle();
c.draw();

同接口不同,子类无需实现 mixin 中已实现了的方法。

4.7. 特殊方法

4.7.1 call()

如果类中实现了 call 方法,则类实例可以像函数一样调用,而实际上执行的就是实例中的 call(...) 方法,如:

class CallableClassWithoutArgument {
    String output = "Callable class";

    void call() {
        print(output);
    }
}

class CallableClassWithArgument {
    call(String name) => print("$name");
}

var withoutArgument = CallableClassWithoutArgument();
withoutArgument(); // Callable class

var withArgument = CallableClassWithArgument();
print(withArgument("John Smith")); //  John Smith

4.7.2 操作符重载方法

Dart 类中可以重载以下操作符:<, >, <=, >=, -, +, /, ~/, *, %, |, ^, &, <<, >>, [], []=, ~, ==

重载方法是在类中实现 operator 及操作符号为名的方法,例如:

class MyNumber {
    num val;
    num operator + (num n) => val * n;
    MyNumber(this.val);
}

MyNumber mn = MyNumber(5);
print(mn+2); // 10

这里,将 + 功能重载为乘法功能。

5. 包

每个 dart 文件都是一个包 package。

import 'URL' 的形式导入其它包,其中的 URL 的形式为 schema:path, schema 的值有:

  • dart: Dart 内置的包,如 import 'dart:math'
  • package: 第三方包,如 import 'package:flutter/material.dart'
  • 没有 schame部分:表示只导入当前项目中的相关包,如 import 'NotesModel.dart'

可以用 show 关键字限定只导入包中相关属性,如 import "NotesModel.dart" show NotesModel, notesModel;。 也可以用 hide 关键字限定只排除包中相关属性,其它属性都导入,如 import "NotesModel.dart" show NotesModel, notesModel;

如果包中的属性名有冲突,可以用 as 关键字将导入的包重命名,如 import 'dart:math' as math;

资源

]]>
Ubuntu 上开发 Flutter 设置 2020-03-31T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/flutter-init 1. 安装 flutter SDK

可以直接 git clone flutter 的 github 库,或从官网下载压缩包,但都比较慢,最好从 中文镜像 下载压缩包。

解压到 $HOME/opt/flutter/,并更新路径: export PATH="$PATH:$HOME/opt/flutter/bin"

国内下载安装 flutter 相关包会比较慢,最好设置从国内镜像下载,在 ~/.bashrc 中设置:

# flutter
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
export PATH="$PATH:$HOME/opt/flutter/bin"

下载解压后,运行 flutter doctor 检测 flutter 安装情况。

doctor 命令会列出相关的问题列表,如未安装 Android Studio, Android Studio 中未安装相关的 SDK 或工具等,逐个修复。

2. 安装 Android Studio

下载安装 Android StudioFile > Settings > Plugins 安装 Flutter 和 Dart 插件。

如果在插件搜索页中无法搜索插件,则需要 FanQiang。

创建虚拟设备

Tools > Avd manager 中创建 Pixel 2 API 28 设备。

3. 创建项目

File > New > New Flutter Project... 创建测试项目。

资源

]]>
用 SendGrid 发送免费电子邮件 2020-03-18T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/send-email-with-sendgrid 1. 概述

SendGrid 免费账号可以限额发送 100/天封邮件,虽然比 Mailgun 的每月 10000 封的免费额度少,但胜成注册无需绑定信息卡。

集成 SendGrid 有 SMTP 和 API 两种方式。官方提供了 Python, Java, GO, Node.js, Ruby, PHP, C# 等语言的 API 库。

2. 注册

注册页 中会有显示 reCAPTCHA 验证,若无显示,需要科学上网。

3. 集成测试

本文示例使用 Python 3.8 发送邮件。

3.1. 通过 API 库集成

注册后在 [api keys](https://app.sendgrid.com/settings/api_keys) 设置页面创建应用和 API KEY。

官方的 python API 库是 sendgrid-python, 通过 pip 安装:

$ pip install sendgrid

测试如下:

import sendgrid
import os
from sendgrid.helpers.mail import *

def send_via_api():
    SENDGRID_API_KEY = "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"
    sg = sendgrid.SendGridAPIClient(api_key=SENDGRID_API_KEY)
    from_email = Email("test@example.com")
    to_email = To("jiang.haiyun@qq.com")
    subject = "Sending with SendGrid is Fun"
    content = Content("text/plain", "and easy to do anywhere, even with Python")
    mail = Mail(from_email, to_email, subject, content)
    response = sg.client.mail.send.post(request_body=mail.get())
    print(response.status_code)
    print(response.body)
    print(response.headers)

send_via_api()
202
b''
Server: nginx
Date: Wed, 18 Mar 2020 04:46:11 GMT
Content-Length: 0
Connection: close
X-Message-Id: yrcMLevLRju8p9cEz4cUFg
Access-Control-Allow-Origin: https://sendgrid.api-docs.io
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Authorization, Content-Type, On-behalf-of, x-sg-elas-acl
Access-Control-Max-Age: 600
X-No-CORS-Reason: https://sendgrid.com/docs/Classroom/Basics/API/cors.html

获取到的状态码 202 表示服务器已接收发送邮件请求。

3.2. 通过 SMTP 集成

注册后在 SMTP 配置页 创建 API key,创建后会有 SMTP 相关的配置信息,如:

Server	smtp.sendgrid.net
Ports	25, 587	(for unencrypted/TLS connections) 465	(for SSL connections)
Username	apikey
Password	your_api_key_value

之后可以连接 SMTP 服务器 smtp.sendgrid.net 来发送邮件,其中用户名为 apikey, 密码为 your_api_key_value

测试如下:

import smtplib
from email.mime.text import MIMEText
from email.header import Header

def send_via_smpt():
    from_addr = "test@example.com"
    to_addr = "test@example.com"
    password = SENDGRID_API_KEY ="YOUR_API_KEY_VALUE"
    smtp_server = "smtp.sendgrid.net"
    username = "apikey"
    subject = "Sending with SendGrid is Fun"

    msg = MIMEText('hello, send by Python...', 'plain', 'utf-8')
    msg['Subject'] = Header(subject, 'utf-8')

    server = smtplib.SMTP(smtp_server, 587)
    server.set_debuglevel(1)
    server.login(username, password)
    server.sendmail(from_addr, [to_addr], msg.as_string())
    server.quit()

send_via_smpt()

资源

]]>
HTTP Basic, Session, Token 三种认证方法简介 2020-03-13T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/http-authentications-basic-session-token 1. 概述

本文简介 HTTP Basic,Session,Token 三种认证方法。

  • Basic 认证:户籍部门已给你签发了一张身份证。你每次去办事,都要带上身份证证,后台要拿你的身份证去系统上查一下。
  • Session 认证:户籍部门已给你签发了一张身份证,但只告诉你身份证号码。你每次去办事,只要报出你的身份证号码,后台要查一个即否有效。
  • Token 认证:户籍部门已给你签发了一张有防伪功能的身份证。你每次去办事,只要出示这张卡片,它就知道你一定是自己人。

2. HTTP Basic 认证

这是一种最基本的认证方法。

在这种认证方法下,用户每次发送请求时,请求头中都必须携带能通过认证的身份信息。

其交互过程如下:

  1. 开始时,客户端发送未携带身份信息的请求。
  2. 服务端返回 401 Unauthorized 状态,并在返回头中说明要用 Basic 方法进行认证: WWW-Authenticate: Basic
  3. 客户端重新发送请求,并将身份信息包含在请求头中: Authorization: Basic aHk6bXlwYXNzd29yZA==
  4. 服务端验证请求头中的身份信息,并相应返回 200 OK 或 403 Forbidden 状态。
  5. 之后,客户端每次发送请求都在请求头中携带该身份信息。
客户端                                                          服务端
------                                                          ------

1----------------------------------------->
GET / HTTP/1.1

                                           <-------------------------2
                                            HTTP/1.1 401 Unauthorized
                                            WWW-Authenticate: Basic

3----------------------------------------->
GET / HTTP/1.1
Authorization: Basic aHk6bXlwYXNzd29yZA==

                                            <------------------------4
                                                       HTTP/1.1 200 OK

5----------------------------------------->
GET /another-path/ HTTP/1.1
Authorization: Basic aHk6bXlwYXNzd29yZA==

其中传送的身份信息是 <username>:<password> 经 base64 编码后的字串。如本例中的 aHk6bXlwYXNzd29yZA==, 经 base64 解码后为 hy:mypassword

这种认证方法的优点是简单,容易理解。

缺点有:

  • 不安全:认证身份信息用明文传送,因此需结合 https 使用。
  • 效率低:服务端处理请求时,每次都需要验证身份信息,如用户名和密码。

3. Session 认证

这种认证方法结合了 Session 和 Cookie。服务端将本次会话信息以 Session 对象的形式保存在服务端的内存、数据库或文件系统中,并将对应的 Session 对象 ID 值 SessionID 以 Cookie 形式返回给客户端,SessionID 保存在客户端的 Cookie 中。

这是一种有状态的认证方法:服务端保存 Session 对象,客户端以 Cookie 形式保存 SessionID。

其交互过程如下:

  1. 客户端在登录页面输入身份信息,如用户名/密码。
  2. 服务端验证身份信息,通过后生成一个 Session 对象,保存到服务端,并将 SessionID 值以 Cookie 形式返回给客户端。
  3. 客户端将接收到的 SessionID 保存到 Cookie 中,并且之后每次请求都在请求头中携带 SessionID Cookie。
  4. 服务端从请求的 Cookie 中获取 SessionID,并查询其对应的 Session 对象,从而获得身份信息。
  5. 客户端退出本次会话后,客户端删除 SessionID 的 Cookie,服务端删除 Session 对象。
  6. 如果客户端之后要重新登录,需重新生成 Session 对象和 SessionID。

优点:

  • 较安全:客户端每次请求时无需发送身份信息,只需发送 SessionID。
  • 较高效:服务端无需每次处理请求时都要验证身份信息,只需通过 SessionID 查询 Session 对象。

缺点:

  • 扩展性差,Session 对象保存在服务端,如果是保存在多个服务器上,有一致性问题,如果保存在单个服务器上,无法适应用户增长。
  • 基于 Cookie 的 SessionID 不能跨域共享,同一用户的多个客户端(如浏览器客户端和 APP)不能共享 SessionId。
  • 基于 Cookie 的 SessionID 易被截获生成 CSRF 攻击。

4. Token 认证

这是一种 SPA 应用和 APP 经常使用的认证方法。它是一种无状态的认证方法。

客户端首先将用户信息发送给服务端,服务端根据用户信息+私钥生成一个唯一的 Token 并返回给客户端。Token 只保存在客户端,之后客户端的每个请求头中都携带 Token,而服务端只通过运算(无需查询)来验证用户。

客户端                                                          服务端
------                                                          ------

1----------------------------------------->
GET / HTTP/1.1

                                           <-------------------------2
                                            HTTP/1.1 401 Unauthorized
                                            WWW-Authenticate: Token

3----------------------------------------->
GET / HTTP/1.1
Authorization: Token f613d789819ff93537ee6a

                                            <------------------------4
                                                       HTTP/1.1 200 OK

5----------------------------------------->
GET /another-path/ HTTP/1.1
Authorization: Token f613d789819ff93537ee6a

优点:

  • Token 只保存在客户端,因此不影响服务端扩展性。
  • 为用户生成的 Token 可以在多个客户端共用。

缺点:

  • Token 包含了用户的全部信息,不只是如 SessionID 类似的一个 ID 值,因此会增加每次请求包的大小。

目前使用较多的是基于JWT(JSON Web Tokens) 的 Token 认证法。

资源

]]>
Django 中自定义用户模型及集成认证授权功能总结 2020-03-13T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/django-custom-user-and-authentication-authorization 1. 概述

Django 中的 django.contrib.auth 应用提供了完整的用户及认证授权功能。

Django 官方推荐基于内置 User 数据模型创建新的自定义用户模型,方便添加 birthday 等新的用户字段和功能。

本文包含的内容有:

  • 介绍在 Django 中如何自定义用户模型,并集成到系统。
  • 定制 django.contrib.auth 应用使用的模板文件。
  • 在系统中集成认证与授权功能。

以下所有示例在 Python 3.8.2 + Django 2.1 中实现。

2. 自定义用户模型

2.1. 创建认证与授权相关的单独应用 accounts

$ python manage.py startapp accounts

将应用添加到项目中:

# project_dir/settings.py
INSTALLED_APPS = [
    # Local
    'accounts.apps.AccountsConfig',
    #...
]

2.2. 创建自定义用户模型 CustomUser

Django 官方文档中推荐基于 AbstractBaseUser 创建自定义用户模型,但是一般基于 AbstractUser 创建再方便。

本命中的自定义 CustomUser 中新增了字段 birthday。

# accounts/models.py

from django.db import models
from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):

    birthday = models.DateField(null=True, blank=True)

2.3. 集成自定义用户模型

通过 AUTH_USER_MODEL 告诉系统新的用户模型:

# project_dir/settings.py
AUTH_USER_MODEL = 'accounts.CustomUser'

之后可通过 get_user_model() 获取该自定义用户模型:

# in view or model files
from django.contrib.auth import get_user_model

CustomUser = get_user_model()

django.contrib.auth 应用已实现了完整的 login, logout 功能,并已在 django.contrib.auth.urls 中定义了 login, logout, password_change, password_change_done, password_reset, password_reset_done, password_reset_confirm, password_reset_complete 等 URL。

django.contrib.auth.urls 集成到项目中:

# project_dir/urls.py
from django.urls import path, include
urlpatterns = [
   path('accounts/',  include('django.contrib.auth.urls'),
   #...
]

集成后,即可访问 /accounts/login/, /accounts/logout/, /accounts/password_change/ 等功能,同时时视图和模板中也可访问这个 URL 定义:

<!-- in template files -->

<a href="{% url 'login' %}">Login URL</a>

# in view files
from django.urls import reverse, reverse_lazy

login_url = reverse('login')

#in Class Based View:
login_url = reverse_lazy('login')

2.4. 集成自定义用户模型到后台管理界面

后台管理界面中,添加新用户时呈现的表单由 django.contrib.auth.forms.UserCreationForm 提供,而更新用户时呈现的表单由 django.contrib.auth.forms.UserChangeForm 提供。

为自定义用户模型定制这两个表单:

# accounts/forms.py
from django.contrib.auth.forms import UserCreationForm, UserChangeForm

from .models import CustomUser

class CustomUserCreationForm(UserCreationForm):


    class Meta(UserCreationForm.Meta):

        model = CustomUser
        fields = ('username', 'email', 'birthday', )


class CustomUserChangeForm(UserChangeForm):


    class Meta(UserChangeForm.Meta):

        model = CustomUser
        fields = UserChangeForm.Meta.fields + ( 'birthday', )

注册到 admin 中:

# accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from .models import CustomUser
from .forms import CustomUserCreationForm, CustomUserChangeForm

class CustomUserAdmin(UserAdmin):

    model = CustomUser

    add_form = CustomUserCreationForm
    form = CustomUserChangeForm

    list_display = ['email', 'username', 'birthday', 'is_staff']

admin.site.register(CustomUser, CustomUserAdmin)

3. 定制 django.contrib.auth 应用使用的模板文件

3.1. 定义模板文件

django.contrib.auth 中 login, logout, password_change, password_change_done, password_reset, password_reset_done, password_reset_confirm, password_reset_complete 等视图,访问的相应模板需保存在registration/ 目录下,模板文件有: login.html, password_change_done.html, password_change_form.html, password_reset_complete.html, password_reset_confirm.html, password_reset_done.html, password_reset_form.html 等。

默认配置下,模板文件需保存在 <app-name>/templates/<app-name>/registration/ 目录下,如 accounts/templates/accounts/registration/login.html

对于小项目,可以将模板目录设置为扁平化:

# project_dir/settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')], # new
        #...
    }
]

从而模板可以保存在 templates/ 目录中,如 templates/registration/login.html

模板中可使用 form 变量,例如:

<!-- templates/registration/login.html -->

{% extends 'base.html' %}

{% block title %}Login{% endblock title %}

{% block content %}
<h2>Login</h2>
<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button class="btn btn-success ml-2" type="submit">Login</button>
</form>
{% endblock content %}

3.2. 创建注册功能

django.contrib.auth 没有实现 sign up 功能。

基于 CreateView 创建 SignUpView 视图:

# accounts/views.py
from django.views.generic.edit import CreateView
from django.urls import reverse_lazy

from .models import CustomUser
from .forms import CustomUserCreationForm

class SignupView(CreateView):
    #model = CustomUser
    form_class = CustomUserCreationForm
    template_name = 'signup.html'

    success_url = reverse_lazy('login')

添加模板文件:

<!-- templates/signup.html -->

{% extends 'base.html' %}

{% load crispy_forms_tags %}
{% block title %}Signup{% endblock title %}

{% block content %}
<h2>Signup</h2>
<form method="post">
  {% csrf_token %}
  {{ form|crispy }}
  <button class="btn btn-success" type="submit">Signup</button>
</form>
{% endblock content %}

添加应用级别的 URL 配置:

# accounts/urls.py
from django.urls import path

from .views import SignUpView

urlpatterns = [
    path('signup/', SignUpView.as_view(), name='signup'),
]

集成到项目级别的 URL 配置中:

# project_dir/urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('accounts.urls')),
    path('accounts/', include('django.contrib.auth.urls')),
    #...
]

4. 在系统中集成认证与授权功能

4.1. 认证:要求登录后才能访问

视图应继承加入 LoginRequiredMixin,属性值 login_url 设置当没有登录时,将转向的登录页面地址或 URL name:

# in view files

from django.views.generic.edit import DeleteView
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied

from .models import Article

class ArticleDeleteView(LoginRequiredMixin, DeleteView):

    model = Article
    template_name = 'article_delete.html'
    success_url = reverse_lazy('article_list')
    login_url = 'login'

4.2. 授权:只有特定的用户或权限才能访问

CBV 中,代码调用入口是 dispatch() 方法,可以在该方法中实现权限验证,当权限不够时抛出异常:

# in view files

from django.views.generic.edit import DeleteView
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied

from .models import Article

class ArticleDeleteView(LoginRequiredMixin, DeleteView):

    model = Article
    template_name = 'article_delete.html'
    success_url = reverse_lazy('article_list')
    login_url = 'login'

    def dispatch(self, request, *args, **kwargs):
        obj = self.get_object()
        if obj.author != request.user:
            raise PermissionDenied()

        return super().dispatch(request, *args, **kwargs)

资源

]]>
Ubuntu 系统上 Python 项目开发本地虚拟环境管理方案: pyenv + pipenv 2020-03-11T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/python-environment-with-pyenv-pipenv 1. 概述

本文介绍用 Pyenv + Pipenv 管理 Python 项目开发的本地虚拟环境。

  • pyenv: 安装和管理多个 Python 版本。
  • pipenv: 为每个项目创建独立的虚拟环境。

以下所有操作在 Ubuntu 16.04 系统上进行。

2. Python 版本管理: pyenv

2.1. 安装 pyenv

$ curl https://pyenv.run | bash

pyenv 相关的内容会安装在 ~/.pyenv/ 目录下。

安装后根据提示将以下内容添加到 ~/.bashrc:

export PATH="~/.pyenv/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"

升级 pyenv:

$ pyenv update

删除 pyenv:

$ rm -rf ~/.pyenv

并删除 ~/.bashrc 中的相关环境变量。

2.2. 安装和管理多个 Python

查看可安装的版本:

$ pyenv install --list

安装指定版本:

$ pyenv install 3.8.2

安装 python 前,要先安装编译 python 所需的依赖包:

$ sudo apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev \
    libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev \
    xz-utils tk-dev libffi-dev liblzma-dev python-openssl git

Common build problems, 不然编译后导入某些 python 库时会出现 ModuleNotFoundError: No module named '_sqlite3' 等问题。

查看当前已安装的 python 版本:

$ pyenv versions
* system (set by /home/hy/.pyenv/version)
  3.8.2

通过 pyenv 安装的所有 Python 版本都保存在 ~/.pyenv/versions/ 目录下。

2.3. 每个目录可指定执行特定的 Python 版本

没有指定前,系统默认的 Python 为 2.7:

$ mkdir test
$ cd test
$ python
Python 2.7.12 (default, Oct  8 2019, 14:14:10) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> 

通过 pyenv local 命令指定,当在该目录下执行 python 时,执行的 python 版本:

$ pyenv local 3.8.2

$ ls -la
total 12
drwxrwxr-x  2 hy hy 4096 3月  10 16:04 .
drwxrwxr-x 42 hy hy 4096 3月  10 13:02 ..
-rw-rw-r--  1 hy hy    6 3月  10 16:03 .python-version

$ cat .python-version 
3.8.2

local 命令会在当前目录下生成一个包含版本号的隐藏文件 .python-version

验证执行的 python 版本:

$ python
Python 3.8.2 (default, Mar 10 2020, 13:47:49) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 

2.4. 切换全局 Python 版本

$ pyenv global 3.8.2

$ python
Python 3.8.2 (default, Mar 10 2020, 13:47:49) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 

3. 虚拟环境管理: pipenv

3.1. 安装 pipenv

确保安装了最新的 3.x 版本 python 和 pip

$ python -V
Python 3.8.2

$ pip -V
pip 19.2.3 from /home/hy/.pyenv/versions/3.8.2/lib/python3.8/site-packages/pip (python 3.8)

安装:

$ pip install pipenv

升级:

$ pip install --upgrade pipenv

3.2. 为每个项目创建独立的虚拟环境

创建项目目录:

$ mkdir django_test && cd django_test

export PIPENV_VENV_IN_PROJECT=1 添加到 ~/.bashrc,要想使配置生效,执行下 source ~/.bashrc, 之后 pipenv 管理的虚拟环境都会安装在项目根目录下的 .venv 目录中。

创建虚拟环境:

$ pipenv --python 3.8

Creating a virtualenv for this project…
Pipfile: /home/hy/workspace/temp/django_test/Pipfile
Using /home/hy/.pyenv/versions/3.8.2/bin/python (3.8.2) to create virtualenv…
⠸ Creating virtual environment...created virtual environment CPython3.8.2.final.0-64 in 178ms
  creator CPython3Posix(dest=/home/hy/workspace/temp/django_test/.venv, clear=False, global=False)
  seeder FromAppData(download=False, pip=latest, setuptools=latest, wheel=latest, via=copy, app_data_dir=/home/hy/.local/share/virtualenv/seed-app-data/v1)
  activators BashActivator,CShellActivator,FishActivator,PowerShellActivator,PythonActivator,XonshActivator

✔ Successfully created virtual environment! 
Virtualenv location: /home/hy/workspace/temp/django_test/.venv
Creating a Pipfile for this project…

$ ls -la
total 16
drwxrwxr-x  3 hy hy 4096 3月  11 12:15 .
drwxrwxr-x 42 hy hy 4096 3月  10 13:02 ..
-rw-rw-r--  1 hy hy  138 3月  11 12:15 Pipfile
drwxrwxr-x  4 hy hy 4096 3月  11 12:15 .venv

其中自动生成的 Pipfile 生成中保存了 pypi 源的 URL:

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]

[requires]
python_version = "3.8"

可以将源 URL 设置为国内的镜像地址来提高下载速度:

[[source]]
name = "pypi"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
verify_ssl = true

[dev-packages]

[packages]

[requires]
python_version = "3.8"

安装依赖包:

$ pipenv install "django==2.1"
Installing django==2.1…
Adding django to Pipfile's [packages]…
✔ Installation Succeeded 
Pipfile.lock not found, creating…
Locking [dev-packages] dependencies…
Locking [packages] dependencies…
✔ Success! 
Updated Pipfile.lock (a5a621)!
Installing dependencies from Pipfile.lock (a5a621)…
  🐍   ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 2/2 — 00:00:01
To activate this project's virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.

安装测试环境下的依赖包:

$ pipenv install pytest --dev

显示已安装的依赖包关系:

$ pipenv graph
Django==2.1
  - pytz [required: Any, installed: 2019.3]
pytest==5.3.5
  - attrs [required: >=17.4.0, installed: 19.3.0]
  - more-itertools [required: >=4.0.0, installed: 8.2.0]
  - packaging [required: Any, installed: 20.3]
    - pyparsing [required: >=2.0.2, installed: 2.4.6]
    - six [required: Any, installed: 1.14.0]
  - pluggy [required: >=0.12,<1.0, installed: 0.13.1]
  - py [required: >=1.5.0, installed: 1.8.1]
  - wcwidth [required: Any, installed: 0.1.8]

删除依赖包:

$ pipenv uninstall django
Uninstalling django…
Found existing installation: Django 2.1
Uninstalling Django-2.1:
  Successfully uninstalled Django-2.1

Removing django from Pipfile…
Locking [dev-packages] dependencies…
Locking [packages] dependencies…
Updated Pipfile.lock (91e3b9)!

进入虚拟环境:

$ pipenv shell

4. 项目管理

$ ls -la
total 20
drwxrwxr-x  3 hy hy 4096 3月  11 12:31 .
drwxrwxr-x 42 hy hy 4096 3月  10 13:02 ..
-rw-rw-r--  1 hy hy  185 3月  11 12:31 Pipfile
-rw-r--r--  1 hy hy 3666 3月  11 12:31 Pipfile.lock
drwxrwxr-x  5 hy hy 4096 3月  11 12:21 .venv

将自动生成的 PipfilePipfile.lock 文件加入版本控制系统,.venv 目录不要加入版本控制系统。

团队成员安装好 pyenv 和 pipenv,在 ~/.bashrc 中配置相应环境变量,clone 项目源码,运行 pipenv install --dev 即可重建虚拟开发环境。

$ cd django_test
$ pipenv install --dev

资源

]]>