分类
未分类

How to Extend Field Widget in Odoo 16

Odoo provides a wide range of field widgets to display different types of data in the user interface. However, there may be cases where you need to customize an existing field widget or create a new one to meet your specific needs.
Example
Let’s create a custom boolean field widget called boolean_badge. This widget will display a badge with the text “Yes” or “No,” depending on the value of the field.
To create the widget, we first need to create a new JavaScript file in our module’s static/src/js directory. In this file, we will import the standardFieldProps object from the @web/views/fields/standard_field_props module. This object contains a set of standard props that are common to all field widgets. We will also import the Component class from the Owl library.import { registry } from “@web/core/registry”;
import { standardFieldProps } from “@web/views/fields/standard_field_props”;
const { Component } = owl;

Next, we need to define a new class called BooleanBadge that extends the Component class. This class will contain the logic for rendering the badge and handling user interactions.

class BooleanBadge extends Component {    setup(){
        this.trueValue = ‘Yes’
        this.falseValue = ‘No’
        this.trueColor = ‘green’
        this.falseColor = ‘red’
        this.defaultColor = ‘white’
    }
    updateValue(val){
        this.props.update(val);
    }
}

In the setup() method, we are setting the default values for the trueValue, falseValue, trueColor, falseColor, and defaultColor props. We can also use the setup() method to initialize any other state that the widget needs.

The updateValue() method is called when the value of the field changes. This method updates the state of the widget and re-renders it.

Now that we have defined the widget class, we need to register it with Odoo. To do this, we add the following code to the end of the JavaScript file.BoolBadge.template = “BoolBadge”
BoolBadge.props = standardFieldProps
BoolBadge.supportedTypes = [“boolean”]
registry.category(“fields”).add(“boolean_badge”,BoolBadge)

The template prop specifies the name of the template file that will be used to render the widget. The props prop specifies the props that are accepted by the widget. The supportedTypes prop specifies the types of fields that the widget can be used with.

Finally, we need to create the template file for the widget. Create a new file in the static/src/xml directory of your module called BooleanBadge.xml. In this file, add the following code:<?xml version=”1.0″ encoding=”UTF-8″ ?> <templates> <t t-name=”BooleanBadge” owl=”1″> <span class=”badge rounded-pill m-2 p-2 border” t-att-class=”props.value ? ‘text-white’ : ‘text-black’ t-esc=”trueValue” t-attf-style=”background-color: {{ props.value ? trueColor : defaultColor}}” t-on-click=”() => this.updateValue(true)”/> <span class=”badge rounded-pill m-2 p-2 border” t-att-class=”props.value ? ‘text-black’ : ‘text-white'” t-esc=”falseValue” t-attf-style=”background-color: {{ props.value ? defaultColor : falseColor}}” t-on-click=”() => this.updateValue(false)”/> </t> </templates>

This template will display a badge with the text “Yes” or “No” depending on the value of the field. The background color of the badge will be determined by the trueColor or falseColor prop, depending on the value of the field.

Once you have created the JavaScript and XML files for the widget, you can use them in your Odoo views. To add the widget to a field, simply add the following attribute to the field:widget=”boolean_badge”

Now, let’s add this widget to the test field inside the product template page.<?xml version=”1.0″ encoding=”UTF-8″ ?>
<odoo>
    <record id=”product_template_only_form_view” model=”ir.ui.view”>
        <field name=”name”>product.template.inherit.coc</field>
        <field name=”model”>product.template</field>
        <field name=”inherit_id”
               ref=”product.product_template_only_form_view”/>
        <field name=”arch” type=”xml”>
            <field name=”detailed_type” position=”after”>
                <field name=”test_field” widget=”boolean_badge”/>
            </field>
        </field>
    </record>
</odoo>

Now, the field looks like as follows

How to Extend Field Widget in Odoo 16-cybrosys
How to Extend Field Widget in Odoo 16-cybrosys

Extending the BooleanBadge widget to add options

You can extend the BooleanBadge widget to add options that allow you to customize the appearance of the badge and the text that is displayed. For example, you could add an option to change the size of the badge, the font of the badge text, or the border of the badge.

To extend the BooleanBadge widget to add options, you can create a new JavaScript file in the static/src/js directory of your module. Then, Import the BooleanBadge widget from your existing module. Define a new class that extends the BooleanBadge widget. Add a new prop to your widget to store the value of the option. Implement any custom behavior for your option, and register your new widget in the registry module.

Example

/** @odoo-module */ import { registry } from “@web/core/registry”; import { standardFieldProps } from “@web/views/fields/standard_field_props”; import { BoolBadge } from “@custom_widget/js/bool_badge”; export class CustomBoolBadge extends BoolBadge { setup() { super.setup(); const options = this.props.options || {}; this.trueValue = options.trueValue || ‘Yes’; this.falseValue = options.falseValue || ‘No’; this.trueColor = options.trueColor || ‘green’; this.falseColor = options.falseColor || ‘red’; this.defaultColor = options.defaultColor || ‘white’; } } CustomBoolBadge.props = { …standardFieldProps, options: { type: Object, optional: true} } CustomBoolBadge.extractProps = ({attrs}) => { return {options: attrs.options} } registry.category(“fields”).add(“custom_bool_badge”, CustomBoolBadge);

To use the CustomBoolBadge widget, you can add it to a view in the same way that you would add any other field widget. For example, the following code would add a CustomBoolBadge widget to the test_field field:<?xml version=”1.0″ encoding=”UTF-8″ ?>
<odoo>
    <record id=”product_template_only_form_view” model=”ir.ui.view”>
        <field name=”name”>product.template.inherit.coc</field>
        <field name=”model”>product.template</field>
        <field name=”inherit_id”
               ref=”product.product_template_only_form_view”/>
        <field name=”arch” type=”xml”>
            <field name=”detailed_type” position=”after”>
                <field name=”test_field” widget=”custom_bool_badge”     options=”{‘trueColor’: ‘blue’, ‘trueValue’: ‘Yes’,
                 ‘falseColor’: ‘yellow’, ‘falseValue’: ‘No’}”/>
            </field>
        </field>
    </record>
</odoo>

In this, we have given trueColor as blue and falseColor as yellow and corresponding values. Let’s see the result.

How to Extend Field Widget in Odoo 16-cybrosys
How to Extend Field Widget in Odoo 16-cybrosys

Now we can see the color has changed to blue and yellow, respectively. 

This is how we can extend and add features to the existing field widgets in Odoo 16.

转自:https://www.cybrosys.com/blog/how-to-extend-field-widget-in-odoo-16

分类
未分类

Docker: How to clear the logs properly for a Docker container?

Use:

truncate -s 0 /var/lib/docker/containers/**/*-json.log

You may need sudo

sudo sh -c "truncate -s 0 /var/lib/docker/containers/**/*-json.log"

转自:https://stackoverflow.com/questions/42510002/docker-how-to-clear-the-logs-properly-for-a-docker-container

分类
未分类

How to SSH into the Docker VM (MobyLinuxVM) on Windows

On Windows, Docker runs in a VM called MobyLinuxVM, but you cannot login to that VM via Hyper-V Manager. We aren’t technically going to SSH into the VM, we’ll create a container that has full root access and then access the file system from there.

  1. Get container with access to Docker Daemon
  2. Run container with full root access
  3. Switch to host file system

Open a Command prompt and execute the following:

docker run –privileged -it -v /var/run/docker.sock:/var/run/docker.sock jongallant/ubuntu-docker-client
docker run –net=host –ipc=host –uts=host –pid=host -it –security-opt=seccomp=unconfined –privileged –rm -v /:/host alpine /bin/sh
chroot /host

view rawssh-info-docker-vm-windows.sh hosted with ❤ by GitHub

You can then execute whatever commands you need to execute.

This code was found on the Docker forums here, thanks to Manuel Patrone for posting it.

摘自:https://blog.jongallant.com/2017/11/ssh-into-docker-vm-windows/

分类
未分类

手把手带你学会Odoo OWL组件开发(1):认识 OWL

在讲解Odoo OWL组件开发之前,我们先来认识一下Odoo OWL概况。

什么是OWL

OWL(Odoo Web Library)是一个由 Odoo为其产品构建的小型 (gzip压缩后<20kb) 前端UI框架,它是一个用 Typescript 编写的,以简单且连贯的方式从 React 和 Vue 中吸取最佳编程思想的现代架构。

OWL是从Odoo14版本开始集成到Odoo中的。OWL设计用于高度动态的应用:

  • 基于XML:模板基于XML格式,这可以创建出有趣的应用。例如,可以在数据库中进行存储并使用xpath动态修改。
  • 浏览器中进行模板编译:这可能并不适合所有应用,但如果需要在浏览器中动态生成用户界面,就非常强大了。例如,通用的表单视图组件可以通过一个XML视图对每个模型生成具体的表单用户界面。
  • 不依赖node:不能使用基于npm的标准web工具。系统更安全。

OWL主要特点是:

  • 声明式渲染
  • 类似于 Vue 的插值表达
  • 生命周期
  • 组件化
  • 异步渲染
  • 类继承与拓展(可以更方便的拓展原有组件)

注意:因为OWL组件是通过ES6类等进行定义,一些ES6的语法在部分老的浏览器中会有问题。因此在使用OWL时,请确保浏览器的版本如Chrome版本在51版本后或者Firefox浏览器53版本后。

快速链接: 目前owl的官方介绍及相关官方文档都在github上 https://github.com/odoo/owl

Odoo为什么要造OWL?

在市场日新月异且用户追求不同的UI体验的今天,前端框架多种多样,像Vue、React和Angular等优秀的前端框架设计,在使用上的相关资源都很丰富,为什么odoo要自己造轮子呢?

官方是这样回答的 :

Odoo 非常模块化,因为odoo的集中性模块化的原因,例如Odoo的核心部分在运行之前不知道将加载/执行哪些文件或者 UI 的状态是什么,因此Odoo不能依赖标准的构建工具链。此外,Odoo 的核心部分需要非常通,Odoo 并不是真正的具有用户界面的应用程序,它是一个生成动态用户界面的应用程序,而且大多数框架都不能胜任这项任务。

在战略上:odoo为什么不适用市面上已有的框架Vue和React?因为目前的开源框架在做出更多的更新和方向变化时,更新的内容和方向与odoo本身的方向有歧义或是无意义的,且一个能控制的相适应的适配的新框架Owl会更符合odoo的需要。

框架上:odoo的模式渲染是通过继承和扩展的方式完成的,这个有他独特的优势

社区上:Vue和React的社区有很多的资源和插件,但是也是因为这些插件,在依赖相关上,odoo是不需要这些相关插件的,如vue中vuex,React中的JSX

模板上:odoo使用的xml模板语法,而目前主流的框架是都不使用xml的,强行使用会很混乱,虽然xml是有很多他诱人的地方,xpath动态修改等(小知识:excel 是可以转成xml的)

编译上: odoo是想最后编译渲染js的,且模板是存储在数据库的,所以odoo是依赖xml的xpath的

综上所述,这些大的小的问题汇聚起来,使用现有的前端框架并不能适用于odoo。

与vue和React的比较

模板差异:Vue的模板语句是需要提前写好的,React则是通过Jsx的方式通过预编译的方式去完成的,而OWL使用了XMl标签助手,可以在类中定义,也可以提前在xml中写好。

异步渲染:在React和Vue中是没有异步钩子函数的,但是在OWL中是有的:

  • willStart(在组件开始渲染之前)
  • willUpdateProps(在设置新道具之前)

但是这也是有风险的,及异步没有接收到返回则页面渲染是有问题的,但是也是很强大的,异步加载文件等。

组件传参:在组件传值上和数据反应上,OWL和Vue的模式一样,而不是用setState方式来改变渲染,prop的方式接受父组件传值,用eventBus方式传值,不足之处是,没有vuex的数据处理中心,这个也是OWL以后可能会加的东西吧。

组件使用开发:目前Vue和React的社区组件库都是十分强大的,而Owl因为是Odoo的前端框架,在社区的开源组件和组件开发上有很多的难点,OWl的组件需要适应odoo中模块开发的模式,上手度和组件单独性上有一定的难点,当然继承扩展也是可以实现的。

类组件:React正在远离类组件,React认为类对开发人员造成了一定的混乱;Odoo认为类和继承是重要的工具,具有继承性的通用组件之间共享代码是Odoo构建其Web客户端的方式,它通常是一个非常简单且合适的解决方案,最重要的是架构决策。此外,Odoo还具有类外组件的另一种特定用途,类的每个方法都为插件提供了扩展点。这可能不是一个干净的体系结构模式,但这是一个务实的决定,可以很好地服务于Odoo,有时用Monkey Patch类来增加外部行为,有点像mixin。

在 Odoo中使用OWL的简单演示

在认识了OWL概况后,在学习之前,我们一起来做一个基础的简单演示。

js全构建方式,

你需要在你自己的模块项目中创建一个component.js文件 我这里项目模块是xc_addons, 路径如下/xc_addons/static/src/js/component.js,

代码如下:

1、第一步

odoo.define(‘my.component’, function (require) {
“use strict”; // 在这里放第3步中的代码});

这里是挂载在odoo上执行

2、第二步,

在你项目的js引入xml文件中引入/xc_addons/views/templates.xml

<template id=”assets_end” inherit_id=”web.assets_backend”>
<xpath expr=”.” position=”after”> <script src=”/my_library/static/src/js/component.js” type=”text/javascript” /> </xpath></template>

3、第三步

在第1步添加的component.js 文件中定义OWL工具类

const { Component, Store } = owl;
const { xml } = owl.tags;const { whenReady } = owl.utils;const { useRef, useDispatch, useState, useStore } = owl.hooks;//定义原型类,class MyComponent extends Component { static template = xml` <div class=”bg-info text-center p-2″> <b> Welcome to Odoo </b> <i class=”fa fa-close p-1 float-right” style=”cursor: pointer;” t-on-click=”onRemove”> </i> </div>` onRemove(ev) { this.destroy(); }}//初始化页面数据,展示数据owl.utils.whenReady().then(() => { const app = new MyComponent(); app.mount(document.body); });

4、就会在全局的body中创建一个 owl的xml元素,页面效果如下

结语

为了与Odoo的运行方式兼容,OWL也具有非常强大的并发模式,同时又非常简单。从上面的例子也能看出来OWL还是很清晰的,大家赶紧动手试试吧!

今天的内容之后我们会陆续的推出关于OWL组件开发的系列的系统教程,敬请期待~!

摘自:https://zhuanlan.zhihu.com/p/498362430

分类
未分类

How do I create an ir.config_parameter record

Jahudiel Contreras

9 7月2021

Best Answer

Hi Alan,

You need to create a new System Parameter.

  1. Activate Developer Mode.
  2. Go to  Settings / Technical / Parameters / System Parameters , click on “Create”.
  3. In the “Key” field put  s2u_online_appointment , and in the “Value” field put public . Click on “Save”.

Please refresh your browser to see applied this change. According to the App description, this should show the expected behavior.

摘自:https://www.odoo.com/zh_TW/forum/bang-zhu-1/how-do-i-create-an-ir-config-parameter-record-190106

分类
未分类

odoo 权限管理学习总结

环境

odoo-14.0.post20221212.tar

base_user_role-12.0.2.1.2.zip

下载地址:

https://apps.odoo.com/apps/modules/12.0/base_user_role/

权限管理

简介

为了更好的熟悉权限,我们先来了解下用户,odoo中的用户分为三类:

  • 内部用户(Internal User): 企业内部的用户,拥有对系统内部的访问权限,也就是说有odoo后端的访问权限。
  • 门户用户(Portal): 非企业内部用户,通常为业务合作伙伴用户,拥有有限的资源访问权限。
  • 公共用户(Public): 面向公众的权限,可以理解为游客权限。

提示:管理员登录系统,激活开发者模式,即可在设置-用户详情页对用户类型进行编辑(Settings -> Users & Companies -> Users)

以上三类用户的信息都存在res_userres_partner表中,那么在odoo中如何区分用户类型以及如何做权限控制的呢?

为了解决上述问题,odoo采用了用户组机制。将用户划分为不同的组(一个用户可以归属多个用户组,一个用户组也可以拥有多个用户),然后给组分配权限,从而实现用户权限的管控及用户类型识别。

以上三种用户分别归属以下用户组:

  • 内部用户:base.group_user
  • 门户用户:base.group_portal
  • 公共用户:base.group_public

odoo也支持自定义用户组(Settings -> Users & Companies -> Groups),并为用户分配不同的用户组,及设置相关权限(菜单权限,视图权限,访问权限,记录规则)

此外,为了更方便的管理用户组,odoo还支持对用户组(group)进行分类:将多个用户组划分为一个用户组分类(category)。

  • 用户组和用户组分类:一个用户组分类可以拥有多个用户组,一个用户组仅归属一个用户组分类,属于1对多的关系。
  • 用户组和用户组的关系:用户组可以被用户组继承(伪继承),当继承某个用户组时,本组用户也会自动加入继承的用户组。如果一个用户属于多个用户组,那么该用户权限为用户组权限的并集,因此设计用户组权限时一定要考虑好组与组之间权限是否会发生冲突。

定义用户组(权限组)

示例:xml数据文件的方式定义菜单权限用户组

<odoo>
    <data noupdate="1">
         <record id="estate_property_menu_groups" model="ir.module.category"><!-- id:供代码或者xml中引用,model:odoo的category模型-->         
             <field name="name">[房地产]模块菜单权限</field><!--用户组分类名称--> 
             <field name="sequence">1</field><!--组分类显示顺序、优先级-->
         </record>

         <!--########################
         [房地产]模块菜单
         ########################-->
        <record id="group_estate_property_root_menu" model="res.groups">
            <field name="name">Real Estate</field><!--用户组名称,阐明组的角色/目的-->
            <field name="category_id" ref="estate_property_menu_groups"/><!--指定用户组所属组分类-->
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/><!--定义用户组继承自哪些组,也就是说该用户组也拥有这些继承组的权限-->
            <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/><!--为用户组添加用户 base.user_root root用户 -->   
        </record>
    </data> 
</odoo>

说明:

noupdate:如果数据文件的内容预期只应用一次(只加载一次,安装或者更新模块时),则可以将noupdate设置为1。如果文件中的部分数据需要应用一次,则可以将文件的这部分放在<data-noupdate="1">中,如下:

<odoo>
    <data noupdate="1">
        <!-- Only loaded when installing the module (odoo-bin -i module) -->
        <operation/>
    </data>
    <!-- (Re)Loaded at install and update (odoo-bin -i/-u) -->
    <operation/>
</odoo>

参考连接:https://www.odoo.com/documentation/14.0/zh_CN/developer/reference/addons/data.html?highlight=noupdate

base.user_admin :admin用户(ID为2的用户,用户数据定义在odoo\addons\base\data\res_users_data.xml

base.user_root: __system__用户(ID为1的用户,technical admin )

category定义相关数据存储在ir_module_category表中

添加的group,可以在Settings -> Users & Groups -> Groups界面看到,组定义相关数据存储在res_groups表中

eval语法说明

  • (0, 0, values) 从提供的valueS字典创建新记录,形如(0, 0, {'author': user_root.id, 'body': 'one'})
  • (2, ID, values) 使用values字典中的值更新id值=ID的现有记录
  • (2, ID) 删除id=ID这条记录(调用unlink方法,删除数据及整个主从数据链接关系)
  • (3, ID) 删除主从数据的链接关系但是不删除这个记录
  • (4, ID) 为id=ID的数据添加主从链接关系
  • (5) 去除所有的链接关系,也就是循环所有的从数据且调用(3,ID)
  • (6, 0, [IDs]) 用IDs中的记录替换原来链接的记录(相当于先执行(5)再循环执行(4, ID)

拓展:

odoo中有个特殊的组base.group_no_one,需要开启Debug模式才可获取该组权限。可以利用该特性实现隐藏对象需求,比如针对一些常规下不需要显示的特殊字段,为其设置属性groups = "base.group_no_one",可以实现在非Debug模式下隐藏字段在视图中的显示。

菜单访问权限

应用实例

estate/security/security_estate_property_menu_groups.xml

<odoo>
    <data noupdate="1">
         <record id="estate_property_menu_groups" model="ir.module.category">
             <field name="name">[房地产]模块菜单权限</field>
             <field name="sequence">1</field>
         </record>

         <!--########################
         [房地产]模块菜单
         ########################-->
        <record id="group_estate_property_root_menu" model="res.groups">
            <field name="name">Real Estate</field>
            <field name="category_id" ref="estate_property_menu_groups"/>
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
            <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
        </record>

        <record id="group_estate_property_advertisements_menu" model="res.groups">
            <field name="name">Real Estate -> Advertisements</field>
            <field name="category_id" ref="estate_property_menu_groups"/>
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
            <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
        </record>

        <record id="group_estate_property_advertisements_properties_menu" model="res.groups">
            <field name="name">Real Estate -> Advertisements -> Properties</field>
            <field name="category_id" ref="estate_property_menu_groups"/>
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
            <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
        </record>
    </data>
</odoo>

estate/__manifest__.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
{
    'name': 'estate',
    'depends': ['base'],
    'data':[
        'security/security_estate_property_menu_groups.xml',
        //...略
    ]
}
菜单配置

views/estate_menus.xml

<?xml version="1.0"?>
<odoo>
    <menuitem id="test_menu_root" name="Real Estate" web_icon="estate,static/img/icon.png"
    groups="group_estate_property_root_menu">
        <menuitem id="test_first_level_menu" name="Advertisements" groups="group_estate_property_advertisements_menu">
            <menuitem id="estate_property_menu_action" action="link_estate_property_action" groups="group_estate_property_advertisements_properties_menu"/>
            <!--略-->
        </menuitem>
        <!--略-->
    </menuitem>
</odoo>

查看效果

注意:

  1. 实践时发现,通过界面点击,访问一些菜单界面时,会在菜单访问URL(参见菜单访问自动生成的URL)中自动添加model,view_type等参数,也就是说会自动访问模块相关模型,如果此时没有对应模型的访问权限(至少需要 read权限),那么即便拥有对应菜单的访问权限,界面上也看不到对应的菜单,笔者尝试过在浏览器中直接通过菜单链接(形如二级导航菜单http://localhost:8888/web#action=85&cids=1&menu_id=127)访问菜单,发现界面上不会显示任何菜单。菜单访问自动生成的URLhttp://localhost:8888/web#action=85&model=estate.property&view_type=kanban&cids=1&menu_id=70
  2. 通过上述方式实现的菜单访问权限控制,实际是通过控制是否隐藏菜单实现的,也就说,如果知道未授权菜单ID,还是可以通过菜单ID拼接菜单URL进行未授权访问。

模型访问权限(Access Rights,表级别)

当模型中没有定义任何访问权限时,odoo会认为没有任何用户可以访问数据,并在日志中打印:

2022-12-14 09:01:38,994 32508 WARNING odoo odoo.modules.loading: The model estate.property has no access rules, consider adding one. E.g. access_estate_property,access_estate_property,model_estate_property,base.group_user,1,0,0,0 

访问权限被定义为ir.model.access 模型记录。每个访问权限关联一个模型,一个group(针对全局访问,没有组) 和一系列权限:createreadwrite 和unlink(等同于delete)。这些访问权限通常定义在security/ir.model.access.csv文件中。

test.model模型访问权限配置示例

id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_test_model,access_test_model,model_test_model,base.group_user,1,0,0,0
  • id 自定义外部标识,模块中保持唯一,一般命名为 access_模型名称_用户组名称
  • name 自定义ir.model.access的名称,一般命名沿用id取值即可
  • model_id/id 、model_id:id 代指需要应用访问权限的模型。标准格式为 model_<model_name>,其中, <model_name>为模块中_name 替换._后的_name 的值
  • group_id/id 、group_id:id 代指需应用访问权限的组,即指定哪个组拥有如下访问权限,如果指定组不是在当前模块中定义的组,需要指定模块名称,形如module_name.groupName。组名一般命名为group_模型名称_权限,形如group_estate_property_read 。如果 group_id为空,则意味着授权给所有用户(非雇员(employees) ,比如 portal 或者public用户).
  • perm_read,perm_write,perm_create,perm_unlink: 分别代表create(创建), read(只读/查询), write (编辑/更新)和unlink(删除)权限,1表示有访问权限,0-表示无权限

具体到实际应用时,为了更灵活的权限管理,一般会为模型的增删改查操作分别定义权限。

授权给用户的模型访问权限,可通过点击Settings -> Users & Groups -> Users用户详情页Access Rights按钮查看。

应用实例

xml数据文件的方式定义房地产模型访问权限

estate/security/security_estate_property_model_groups.xml

<odoo>
    <data noupdate="1">
         <record id="estate_property_model_groups" model="ir.module.category">
             <field name="name">[房地产]模型权限</field>
             <field name="sequence">1</field>
         </record>


         <!--########################
         [房地产]模型
         ########################-->
        <!-- [房地产]模型 增删改查 -->
        <record id="group_estate_property_read" model="res.groups">
            <field name="name">[房地产]模型 只读</field>
            <field name="category_id" ref="estate_property_model_groups"/>
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
            <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
        </record>

        <record id="group_estate_property_write" model="res.groups">
            <field name="name">[房地产]模型 更新</field>
            <field name="category_id" ref="estate_property_model_groups"/>
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
            <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
        </record>

        <record id="group_estate_property_create" model="res.groups">
            <field name="name">[房地产]模型 创建</field>
            <field name="category_id" ref="estate_property_model_groups"/>
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
            <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
        </record>

        <record id="group_estate_property_delete" model="res.groups">
            <field name="name">[房地产]模型 删除</field>
            <field name="category_id" ref="estate_property_model_groups"/>
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
            <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
        </record>
    </data>
</odoo>

estate/security/ir.model.access.csv

id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_estate_property_group_estate_property_read,access_estate_property_group_estate_property_read,model_estate_property,group_estate_property_read,1,0,0,0
access_estate_property_group_estate_property_write,access_estate_property_group_estate_property_write,model_estate_property,group_estate_property_write,1,1,0,0
access_estate_property_group_estate_property_create,access_estate_property_group_estate_property_create,model_estate_property,group_estate_property_create,1,0,1,0
access_estate_property_group_estate_property_delete,access_estate_property_group_estate_property_delete,model_estate_property,group_estate_property_delete,1,0,0,1

estate/__manifest__.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
{
    'name': 'estate',
    'depends': ['base'],
    'data':[
        'security/security_estate_property_menu_groups.xml',
        'security/security_estate_property_model_groups.xml',
        'security/ir.model.access.csv', 
        //...略
    ]
}
查看效果

打开用户编辑界面

记录规则(Record Rules,记录级别)

记录规则是允许某个操作必须满足的条件。记录规则按照访问权限逐条记录评估。

默认允许的记录规则:如果授予模型访问权限(Access Rights),并且没有规则适用于用户的操作和模型,则授予访问权限

记录规则保存在ir.rule模型表里,我们通过管理ir_rule表中的记录,即可控制记录的访问权限

定义规则

示例:xml数据文件的方式定义房地产模型记录访问规则

<odoo>
    <data noupdate="0">
        <!--########################
        [房地产]模型记录规则
        ########################-->
        <record id="estate_property_record_read_rule" model="ir.rule"><!--id:外部规则id,供代码或者xml中引用 -->
            <field name="name">[房地产]模型记录规则</field>
            <field name="model_id" ref="model_estate_property"/>
            <field name="domain_force">[('create_uid', '=', user.id)]</field><!--仅显示用户自己创建的记录-->
            <field name="groups" eval="[(4, ref('group_estate_property_record_read'))]"/>
            <!--操作权限(仅作用于经domain_force滤后的记录)-->
            <field name="perm_read" eval="1"/>
            <field name="perm_write" eval="1"/>
            <field name="perm_create" eval="1"/>
            <field name="perm_unlink" eval="1"/>
        </record>
    </data>
</odoo>
  • name规则名称
  • model_id需要应用规则的模型,标准格式为 model_<model_name>,其中, <model_name>为模块中_name 替换._后值
  • groups指定规则需要作用、不作用于哪些组(res.groups)。可以指定多个组。如果未指定组,规则为gobal规则。规则与组的关联关系存在rule_group_rel表中
  • global根据“groups”计算,提供了对规则是否全局状态的轻松访问。eval="True"eval="1"则表示全局规则,eval="False"eval="0"则表示非全局规则
  • domain_force指定为 domain的谓词,如果该domain与记录匹配,则规则允许所选操作,否则禁止。可以简单的理解为指定过滤条件,用户只能访问符合本过滤条件的记录,配置为[(1,'=',1)]则表示匹配所有记录。domain是一个可以使用以下变量的python表达式:
    • timePython的 time 模块
    • user以单例记录集(singleton recordset)表示的当前用户
    • company_id当前用户,当前所选的公司的公司id(非记录集)。
    • company_ids当前用户可以访问的公司ID列表(非记录集)。 查看Security rules 获取更多详细信息。

官方文档:

The perm_method have completely different semantics than for ir.model.access: for rules, they specify which operation the rules applies for. If an operation is not selected, then the rule is not checked for it, as if the rule did not exist.

All operations are selected by default

译文:

perm_method 具有与 ir.model.access完全不同的语义:对于规则,它们指定规则需要应用的操作。如果(规则)未选择某个操作,则不会为该操作检查规则,就像该规则不存在一样。

规则默认适用所有操作。

笔者实践发现:

  • 如果创建了规则,但是没有授权给用户,那对于该用户来说,该规则不起作用,就像该规则不存在一样
  • perm_methodeval值不能同时为"False""0",否则会违反 ir_rule表的检查约束ir_rule_no_access_rightsCHECK (perm_read!=False or perm_write!=False or perm_create!=False or perm_unlink!=False)
  • 将任意一个perm_method设置为eval="True"eval="1" ,并将规则授权给用户,规则生效,所以我个人理解,目前记录规则,就是用于过滤记录的,通过domain_force控制哪些记录可以显示给用户

规则默认适用所有操作。

  • perm_create
  • “perm_read`
  • perm_write
  • perm_unlink
  • 授权给用户的记录访问规则,可通过点击Settings -> Users & Groups -> Users用户详情页Record Rules按钮查看。

全局规则(Global rules) VS 组规则(group rules)

全局规则和组规则在组成和组合方式上存在很大差异:

  • 全局规则和全局规则之间取交集,如果两个全局规则都生效,则必须满足两者才能授予访问权限,这意味着添加全局规则总是会进一步限制访问。
  • 组规则和组规则之间取并集,如果两个组规则都生效,则满足其中之一就可以授予访问权限。这意味着添加组规则可以扩展访问,但不能超出全局规则定义的范围。
  • 全局规则集和组规则集之间取交集,这意味着添加到给定全局规则集的第一个组规则将限制访问。

危险提示

创建多个全局规则是有风险的,因为可能创建不重叠的规则集,这将删除所有访问权限

应用实例

estate/security/security_estate_property_model_groups.xml,新增group_estate_property_record_query

<odoo>
    <data noupdate="1">
         <record id="estate_property_model_groups" model="ir.module.category">
             <field name="name">[房地产]模型权限</field>
             <field name="sequence">1</field>
         </record>


         <!--########################
         [房地产]模型
         ########################-->
        <!-- [房地产]模型 增删改查 -->
        <record id="group_estate_property_read" model="res.groups">
            <field name="name">[房地产]模型 只读</field>
            <field name="category_id" ref="estate_property_model_groups"/>
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
            <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
        </record>

        <record id="group_estate_property_write" model="res.groups">
            <field name="name">[房地产]模型 更新</field>
            <field name="category_id" ref="estate_property_model_groups"/>
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
            <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
        </record>

        <record id="group_estate_property_create" model="res.groups">
            <field name="name">[房地产]模型 创建</field>
            <field name="category_id" ref="estate_property_model_groups"/>
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
            <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
        </record>

        <record id="group_estate_property_delete" model="res.groups">
            <field name="name">[房地产]模型 删除</field>
            <field name="category_id" ref="estate_property_model_groups"/>
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
            <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
        </record>
    </data>
  
    <data noupdate="0">
        <!--########################
         [房地产]模型记录
         ########################-->
        <record id="group_estate_property_record_query" model="res.groups">
            <field name="name">[房地产]模型记录 查询</field>
            <field name="category_id" ref="estate_property_model_groups"/>
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
            <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
        </record>       
    </data>
</odoo>

estate/security/security_estate_property_model_record_rules.xml

<odoo>
    <data noupdate="0">
        <!--########################
        [房地产]模型记录规则
        ########################-->
        <record id="estate_property_record_read_rule" model="ir.rule">
            <field name="name">[房地产]模型记录规则</field>
            <field name="model_id" ref="model_estate_property"/>
            <field name="domain_force">[('create_uid', '=', user.id)]</field>
            <field name="groups" eval="[(4, ref('group_estate_property_record_read'))]"/>
            <field name="perm_read" eval="1"/>
            <field name="perm_write" eval="1"/>
            <field name="perm_create" eval="1"/>
            <field name="perm_unlink" eval="1"/>
        </record>
    </data>
</odoo>

estate/__manifest__.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
{
    'name': 'estate',
    'depends': ['base'],
    'data':[        
        'security/security_estate_property_menu_groups.xml',
        'security/security_estate_property_model_groups.xml',
        'security/security_estate_property_model_record_rules.xml',
        'security/ir.model.access.csv',
        //...略
    ]
}

查看效果

参考连接:https://www.odoo.com/documentation/14.0/zh_CN/developer/reference/addons/security.html#record-rules

字段权限(Field Access,字段级别)

ORM字段可以具有提供组列表的groups属性(值为逗号分隔的组XML ID列表,如groups='base.group_user,base.group_system'注意:groups属性值格式:moduleName.groupName,其中moduleNamegroupName组所在模块名称,必不可少

如果当前用户不在列出的组中,他将无权访问该字段:

  • 将自动从请求的视图中删除受限制的字段
  • fields_get()响应中删除受限制的字段

尝试(显式的)读取或写入受限字段会导致访问错误

修改estate\security\security_estate_property_model_groups.xml,添加group_estate_property_selling_price_field

    <data noupdate="0">
        <!--########################
         [房地产]模型记录
         ########################-->
        <record id="group_estate_property_selling_price_field" model="res.groups">
            <field name="name">[房地产]模型 售价字段</field>
            <field name="category_id" ref="estate_property_model_groups"/>
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
            <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
        </record>
        <record id="group_estate_property_record_query" model="res.groups">
            <field name="name">[房地产]模型记录 查询</field>
            <field name="category_id" ref="estate_property_model_groups"/>
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
            <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
        </record>
    </data>

查看效果

修改estate\views\estate_property_views.xml视图selling_price字段,添加groups属性

<field name="selling_price" string="Selling Price" groups="estate.group_estate_property_selling_price_field"/>

验证,发现界面上,未授权上述框选权限的用户已经看不到上述字段了

注意:通过为当前视图中目标字段添加groups属性实现的权限控制仅作用于当前视图,如果希望当前视图模型(Model)的所有视图中,对该字段实现统一的权限控制话,需要在模型定义中,为目标字段添加groups属性,如下

selling_price = fields.Float('selling price', digits=(8, 2), readonly=True, copy=False, groups="estate.group_estate_property_selling_price_field")

参考连接:https://www.odoo.com/documentation/14.0/zh_CN/developer/reference/addons/security.html#field-access

扩展:在页面从数据库加载视图时,会通过load_view接口,会调用fields_view_get方法,可以重写此方法以控制xml显示的效果(参考网络资料,未实践验证)

按钮权限(按钮级别)

类似字段权限控制,仅需在在对应视图中,为目标按钮<button>元素,添加groups属性即可。

角色定义

新增并安装base_user_role模块

base_user_role模块的作用可以简单理解为,按自定义维度将所需权限组组合在一起,组成角色,实现批量授权的功能。

解压下载的base_user_role-12.0.2.1.2.zip文件,对解压后的部分文件做如下修改:

base_user_role\models\user.pybase_user_role\models\role.py

去除上述两个文件中的所有@api.multi修饰符,解决安装报错问题:

AttributeError: module 'odoo.api' has no attribute 'multi'

说明:Odoo 13.0开始,移除multimulti作为默认实现。

base_user_role/views/role.xml

修改

<record model="ir.actions.act_window" id="action_res_users_role_tree">
    <field name="name">Roles</field>
    <field name="type">ir.actions.act_window</field>
    <field name="res_model">res.users.role</field>
    <field name="view_type">form</field>
    <field name="view_id" ref="view_res_users_role_tree"/>
</record>

<record model="ir.actions.act_window" id="action_res_users_role_tree">
    <field name="name">Roles</field>
    <field name="type">ir.actions.act_window</field>
    <field name="res_model">res.users.role</field>
    <field name="view_mode">form</field>
    <field name="view_id" ref="view_res_users_role_tree"/>
</record>

解决安装报错问题:

odoo.tools.convert.ParseError: while parsing file:/d:/codepojects/odoo14/custom/base_user_role/views/role.xml:63, near

然后,将解压目录下base_user_role整个文件夹拷贝odoo14\custom目录下,最后,重启服务并安装该模块。

安装成功后,Settings -> Users & Companies菜单下,将新增Roles子菜单(笔者实践发现无法通过该页面新增角色并关联用户),Settings -> Users & Companies -> Users 用户记录详情页将新增Roles Tab页

新增并安装estate_role模块

为了统一管理权限组,考虑新增一个单独的应用模块estate_role,模块文件组织结构如下

custom/estate_role
│  __init__.py
│  __manifest__.py
│
├─security
│      security_estate_property_menu_groups.xml
│      security_estate_property_model_groups.xml
│      security_roles.xml
│
└─views

odoo14\custom\estate_role\security\security_roles.xml

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <data noupdate="1">
        <record id="group_role_base_user" model="res.users.role">
            <field name="name">基础用户</field>
            <field name="implied_ids" eval="[
                    (4, ref('base.group_user')),
                ]"/>
        </record>

        <record id="group_role_multi_company" model="res.users.role">
            <field name="name">多公司</field>
            <field name="implied_ids" eval="[
                    (4, ref('base.group_multi_company')),
                ]"/>
        </record>

        <record id="group_role_devops" model="res.users.role">
            <field name="name">运维</field>
            <field name="implied_ids" eval="[
                    (4, ref('estate_role.group_estate_property_root_menu')),
                    (4, ref('estate_role.group_estate_property_advertisements_menu')),
                    (4, ref('estate_role.group_estate_property_advertisements_properties_menu')),
                    (4, ref('estate_role.group_estate_property_read')),
                    (4, ref('estate_role.group_estate_property_write')),
                    (4, ref('estate_role.group_estate_property_create'))
                ]"/>
        </record>
    </data>
</odoo>

odoo14\custom\estate_role\__init__.py

文件内容为空

odoo14\custom\estate_role\__manifest__.py

{
    "name": "Estate Roles",
    "license": "LGPL-3",
    "depends": ["base_user_role"],
    "data": [
        "security/security_estate_property_menu_groups.xml",
        "security/security_estate_property_model_groups.xml",
        "security/security_roles.xml"
    ],
    "installable": True,
}

说明:odoo14\custom\estate\__manifest__.py data列表中已去除上述两个groups文件

重启服务并安装estate_role模块

查看效果

用户详情页面,查看用户权限,发现新增 User Roles

编辑用户,勾选图中的角色,保存,发现和角色关联的权限组都会被自动勾选了。

注意:

  • 取消勾选已授予的角色,并保存,不会自动取消勾选角色关联的权限组,即取消授予角色操作,不会取消通过授予角色授予给用户的权限组
  • 已授予角色给用户的情况下,取消勾选某个权限组并保存,如果该权限组和授予给用户的角色关联,则无法取消勾选的权限组,因为角色关联了该权限组
  • 权限页面勾选并保存的角色,不会在用户详情页的Roles Tab页中显示
  • 除了通过在用户详情页-权限(Access Rights)Tab页面,选取角色为用户批量授权外,还可以在用户详情页的Roles Tab页中为用户添加角色来实现批量授权。

参考连接

https://www.odoo.com/documentation/14.0/zh_CN/developer/reference/addons/security.html

转自:https://www.cnblogs.com/shouke/p/17135926.html

分类
未分类

odoo POS 集成微信支付和支付宝支付

最近有小伙伴有这方面的需求,市场上又没有十分靠谱的现成的解决方案,于是花了一天的时间把微信和支付宝的POS集成模块做了出来。这里记录一下开发的大概流程和使用介绍。

支付宝

我们之前是对接过支付宝在线支付的,而且因为支付宝有沙箱环境,因此,对接的比较流畅。我们先去支付宝开发中心注册一个开发者账号,然后可以拿到支付宝的沙箱环境账号和密钥。这里需要一部安卓手机,因为支付宝的沙箱钱包只有安卓版没有苹果版本。

沙箱应用中可以找到 APPID,设置密钥 和商家密钥等关键参数。

根据我们之前对接支付宝在线支付的经验,我们需要的参数有一下几个:

  • App Seller Id: 支付宝商家ID
  • Alipay App Id: 支付宝应用ID
  • Mechant Private Key: 商户私钥
  • Alipay Public Key: 支付宝RSA公钥
  • Sign Type: 加密方式

其中支付宝RSA公钥可以通过支付宝提供的生成工具生成,也可以直接自己生成,具体方法参考官方文档,这里不赘述了。

接下来,我们开始对接。首先,我们要搞清楚POS条码支付的流程(区别于用户主动扫码支付,这里称之为条码支付):

  1. 用户出示条码/二维码,实际是出示了自己条码支付的授权码
  2. 商户在通过扫描到自己的系统中之后,系统通过条码支付接口发起扣款请求
  3. 用户在手机端确认金额
  4. 商户系统扣款成功
  5. 用户收到扣款通知

其中第3步,通常为了支付的便捷,小额支付通常会跳过这一步。

因为支付过程中可能会碰到失败的情况,因此需要商户在系统中获取到用户支付中这个状态的时候,进行主动查询确认,一般经过三次尝试如果仍旧支付不成功时,调用撤销接口取消交易。对应到后台,我们需要三个接口:

  1. 支付接口
  2. 查询接口
  3. 撤销接口

支付接口

1
2
3
4
5
6
7
8
9
def barcode_pay(self, order_no, subject, auth_code, amount):
“””条码支付”””
# 条码支付
alipay = self._get_alipay_client()
_logger.debug(f”POS支付宝条码支付:{order_no},{subject},{amount}”)
res = alipay.pay.trade_pay(
order_no, ‘bar_code’, auth_code, subject, total_amount=amount)
_logger.debug(f”POS支付宝支付结果:{res}”)
return res

查询接口

1
2
3
4
5
6
def query_payment(self, trade_no):
“””查询支付结果”””
alipay = self._get_alipay_client()
res = alipay.pay.trade_query(trade_no=trade_no)
_logger.debug(f”POS支付宝交易查询结果:{res}”)
return res

撤销接口

1
2
3
4
5
6
def cancel_payment(self, trade_no):
“””取消交易”””
alipay = self._get_alipay_client()
res = alipay.pay.trade_cancel(trade_no=trade_no)
_logger.debug(f”POS支付宝交易取消:{trade_no}”)
return res

其中,查询和取消接口均可以使用商户订单号或者支付宝流水号,这里我们为了方便起见统一用的时支付宝流水号。

为了在POS系统中新增一个支付方法,我们需要新增一个支付方法,支付宝,并将我们之前讲到的参数放到设置里:

然后我们在门店的设置里新增这个支付方法:

新增之后,我们就可以在POS界面看到新增的支付方法了。

前端的对接相对比较复杂,我们要继承PaymentInterface对象,新生成一个PaymentAlipay 对象,负责处理前端的业务请求。

其中最主要的方法是处理后端传过来的响应并作出合适的反馈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
_alipay_handle_response: function (response) {
var line = this.pos.get_order().selected_paymentline;
var self = this;
if (response.code == ‘10000’) {
self._alipay_success(response.trade_no)
self.pos.gui.close_popup();
}
else if (response.code == ‘10003’) {
line.set_payment_status(“pending”)
this.pos.gui.show_popup(‘confirm’, {
‘title’: ‘支付中’,
‘body’: ‘等待用户付款’,
‘confirm’: this.check_payment,
‘cancel’: this.cancel_payment,
‘payment_method’: line.payment_method,
‘trade_no’: response.trade_no
});
}
else if (response.code = ‘40004’) {
line.set_payment_status(“pending”)
this.pos.gui.show_popup(‘error’, {
‘title’: ‘支付失败’,
“body”: response.sub_msg
});
}
},

支付宝在支付以后,通常会返回一下几种状态的结果:

  • 10000: 支付成功
  • 10003: 用户支付中
  • 40004: 支付失败

其中 10003代表等待用户确认支付,一般出现在大额支付或者长期未使用的客户中,此时商户应该等待用户支付完成后,进行查询操作,然后完成后续的业务处理。

微信支付

微信支付的整体流程与支付宝一致,区别在于微信支付的金额单位是分,因此我们需要在处理过程中做一些处理,另外,微信的商户订单号并部支持空格,因此对于odoo默认的”Order 12345”这种订单号也需要做适配,同样地,我们在交易查询和交易撤销的接口中也应该使用微信的交易号而不是商户订单号。

效果展示

支付成功:

支付失败:

需要的同学欢迎到我的淘宝购买哦

转自:http://mixoo.cn/2021/01/12/odoo-pos-wechat-alipay/

git:https://github.com/jellyfrank/payment_alipay

分类
未分类

odoo 对接微信支付

前段时间,我们对接了支付宝,这次我们来对接微信支付。

沙箱环境

微信支付没有沙箱环境,要想测试就必须有营业执照。

Python SDK

这里我们使用的是开源SDK库 WechatPy

微信支付

要使用微信支付,你必须要满足一下条件

  • APPID: 一个绑定了微信支付的公众号APPID
  • API KEY: 在申请微信支付后可以设置,用于调用微信接口
  • 商户号: 开通了微信支付的商户编号
  • 商户证书文件: 开通了微信支付的商户证书
  • 商户密钥文件: 跟商户证书匹配的密钥文件

有了这几个参数才可以正确的调用微信支付的接口

1
2
3
4
5
6
7
8
9
10
11
12
def _get_wechatpay(self):
“””获取微信支付客户端”””
try:
# WeChatPay has no sandbox enviroment.
wechatpay = WeChatPay(self.wechatpay_appid,
self.wechatpay_app_key,
self.wechatpay_mch_id,
mch_cert=self.wechatpay_mch_cert,
mch_key=self.wechatpay_mch_key)
return wechatpay
except Exception as err:
_logger.exception(f”生成微信支付客户端失败:{err}”)

odoo的官方商城支付,对应的微信支付是Navtive方式。

微信与支付不同的是,官方并没有给我们提供一个收银台页面,因此,我们需要自己实现一个,并持续监听,等到客户付款完成后再跳转验证页面。

微信的验证也是两种方式,一种是微信官方的推送消息,一种是我们自己根据订单去微信支付服务器查询。(这点不同于支付宝)。

主动查询

主动查询,用到的是wechatpy的query接口,查询起来比较简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@api.model
def wechatpy_query_pay(self, order):
“””
主动去微信支付查询支付结果
用户支付前没有transcation_id因此,只能用商户自有订单号去查
只有SUCCESS支付成功,其他状态均不成功
“””
wechatpay = self._get_wechatpay()
res = wechatpay.order.query(out_trade_no=order)
_logger.info(“主动查询微信支付结果:{}”.format(res))
if res[“return_code”] == “SUCCESS” and res[“result_code”] == “SUCCESS”:
if res[“trade_state”] == “SUCCESS”:
transaction = self.env[“payment.transaction”].sudo().search(
[(‘reference’, ‘=’, order)], limit=1)
if transaction.state in (‘draft’, ‘pending’, ‘authorized’):
# 将支付结果设置完成
result = {
“acquirer_reference”: res[‘transaction_id’]
}
transaction.write(result)
transaction._set_transaction_done()
return True
elif transaction.state == ‘done’:
return True
else:
return False
return False

需要说明的是,我们再验证了支付之后,需要将对应的支付事务完成。

微信消息处理

客户在支付完成后,微信官方会给我们传入的回调地址发送业务通知。这点wechatpy也帮我们封装好了方法,使用起来也很方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def _verify_wechatpay(self, data):
“””验证微信支付服务器返回的信息”””
try:
wechatpay = self._get_wechatpay()
result = wechatpay.parse_payment_result(data)
_logger.info(“解析微信支付返回结果:{}”.format(result))
if result[‘result_code’] == ‘SUCCESS’ and result[‘return_code’] == ‘SUCCESS’:
# 支付校验成功
transaction = self.env[“payment.transaction”].sudo().search(
[(‘reference’, ‘=’, result[“out_trade_no”])], limit=1)
if transaction.state in (‘draft’, ‘pending’, ‘authorized’):
# 将支付结果设置完成
result = {
“acquirer_reference”: result[‘transaction_id’]
}
transaction.write(result)
transaction._set_transaction_done()
return True
elif transaction.state == ‘done’:
return True
else:
return False
return False
except Exception as err:
_logger.error(“解析微信支付推送消息失败:{}”.format(traceback.format_exc()))
return False

在验证了微信的消息之后,同样也要将支付事务设置为完成状态。

完整示例

模块开源,下载地址

转自:http://mixoo.cn/2019/11/26/odoo-wechat-pay/

分类
未分类

Configure endpoints for the ASP.NET Core Kestrel web server

  • Article
  • 06/27/2023
  • 11 contributors

Feedback

In this article

  1. ConfigureEndpointDefaults(Action<ListenOptions>)
  2. Configure(IConfiguration)
  3. ConfigureHttpsDefaults(Action<HttpsConnectionAdapterOptions>)
  4. ListenOptions.UseHttps

Show 13 more

 Note

This isn’t the latest version of this article. For the current release, see the .NET 7 version of this article.

ASP.NET Core projects are configured to bind to a random HTTP port between 5000-5300 and a random HTTPS port between 7000-7300. This default configuration is specified in the generated Properties/launchSettings.json file and can be overridden. If no ports are specified, Kestrel binds to:

  • http://localhost:5000
  • https://localhost:5001 (when a local development certificate is present)

Specify URLs using the:

  • ASPNETCORE_URLS environment variable.
  • --urls command-line argument.
  • urls host configuration key.
  • UseUrls extension method.

The value provided using these approaches can be one or more HTTP and HTTPS endpoints (HTTPS if a default cert is available). Configure the value as a semicolon-separated list (for example, "Urls": "http://localhost:8000;http://localhost:8001").

For more information on these approaches, see Server URLs and Override configuration.

A development certificate is created:

The development certificate is available only for the user that generates the certificate. Some browsers require granting explicit permission to trust the local development certificate.

Project templates configure apps to run on HTTPS by default and include HTTPS redirection and HSTS support.

Call Listen or ListenUnixSocket methods on KestrelServerOptions to configure URL prefixes and ports for Kestrel.

UseUrls, the --urls command-line argument, urls host configuration key, and the ASPNETCORE_URLS environment variable also work but have the limitations noted later in this section (a default certificate must be available for HTTPS endpoint configuration).

KestrelServerOptions configuration:

ConfigureEndpointDefaults(Action<ListenOptions>)

Specifies a configuration Action to run for each specified endpoint. Calling ConfigureEndpointDefaults multiple times replaces prior Actions with the last Action specified:

C#Copy

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureEndpointDefaults(listenOptions =>
    {
        // ...
    });
});

 Note

Endpoints created by calling Listen before calling ConfigureEndpointDefaults won’t have the defaults applied.

Configure(IConfiguration)

Enables Kestrel to load endpoints from an IConfiguration. The configuration must be scoped to the configuration section for Kestrel. The Configure(IConfiguration, bool) overload can be used to enable reloading endpoints when the configuration source changes.

By default, Kestrel configuration is loaded from the Kestrel section and reloading changes is enabled:

JSONCopy

{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:5000"
      },
      "Https": {
        "Url": "https://localhost:5001"
      }
    }
  }
}

If reloading configuration is enabled and a change is signaled then the following steps are taken:

  • The new configuration is compared to the old one, any endpoint without configuration changes are not modified.
  • Removed or modified endpoints are given 5 seconds to complete processing requests and shut down.
  • New or modified endpoints are started.

Clients connecting to a modified endpoint may be disconnected or refused while the endpoint is restarted.

ConfigureHttpsDefaults(Action<HttpsConnectionAdapterOptions>)

Specifies a configuration Action to run for each HTTPS endpoint. Calling ConfigureHttpsDefaults multiple times replaces prior Actions with the last Action specified.

C#Copy

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        // ...
    });
});

 Note

Endpoints created by calling Listen before calling ConfigureHttpsDefaults won’t have the defaults applied.

ListenOptions.UseHttps

Configure Kestrel to use HTTPS.

ListenOptions.UseHttps extensions:

  • UseHttps: Configure Kestrel to use HTTPS with the default certificate. Throws an exception if no default certificate is configured.
  • UseHttps(string fileName)
  • UseHttps(string fileName, string password)
  • UseHttps(string fileName, string password, Action<HttpsConnectionAdapterOptions> configureOptions)
  • UseHttps(StoreName storeName, string subject)
  • UseHttps(StoreName storeName, string subject, bool allowInvalid)
  • UseHttps(StoreName storeName, string subject, bool allowInvalid, StoreLocation location)
  • UseHttps(StoreName storeName, string subject, bool allowInvalid, StoreLocation location, Action<HttpsConnectionAdapterOptions> configureOptions)
  • UseHttps(X509Certificate2 serverCertificate)
  • UseHttps(X509Certificate2 serverCertificate, Action<HttpsConnectionAdapterOptions> configureOptions)
  • UseHttps(Action<HttpsConnectionAdapterOptions> configureOptions)

ListenOptions.UseHttps parameters:

  • filename is the path and file name of a certificate file, relative to the directory that contains the app’s content files.
  • password is the password required to access the X.509 certificate data.
  • configureOptions is an Action to configure the HttpsConnectionAdapterOptions. Returns the ListenOptions.
  • storeName is the certificate store from which to load the certificate.
  • subject is the subject name for the certificate.
  • allowInvalid indicates if invalid certificates should be considered, such as self-signed certificates.
  • location is the store location to load the certificate from.
  • serverCertificate is the X.509 certificate.

In production, HTTPS must be explicitly configured. At a minimum, a default certificate must be provided.

Supported configurations described next:

  • No configuration
  • Replace the default certificate from configuration
  • Change the defaults in code

No configuration

Kestrel listens on http://localhost:5000 and https://localhost:5001 (if a default cert is available).

Replace the default certificate from configuration

A default HTTPS app settings configuration schema is available for Kestrel. Configure multiple endpoints, including the URLs and the certificates to use, either from a file on disk or from a certificate store.

In the following appsettings.json example:

  • Set AllowInvalid to true to permit the use of invalid certificates (for example, self-signed certificates).
  • Any HTTPS endpoint that doesn’t specify a certificate (HttpsDefaultCert in the example that follows) falls back to the cert defined under Certificates:Default or the development certificate.

JSONCopy

{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:5000"
      },
      "HttpsInlineCertFile": {
        "Url": "https://localhost:5001",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      },
      "HttpsInlineCertAndKeyFile": {
        "Url": "https://localhost:5002",
        "Certificate": {
          "Path": "<path to .pem/.crt file>",
          "KeyPath": "<path to .key file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      },
      "HttpsInlineCertStore": {
        "Url": "https://localhost:5003",
        "Certificate": {
          "Subject": "<subject; required>",
          "Store": "<certificate store; required>",
          "Location": "<location; defaults to CurrentUser>",
          "AllowInvalid": "<true or false; defaults to false>"
        }
      },
      "HttpsDefaultCert": {
        "Url": "https://localhost:5004"
      }
    },
    "Certificates": {
      "Default": {
        "Path": "<path to .pfx file>",
        "Password": "$CREDENTIAL_PLACEHOLDER$"
      }
    }
  }
}

 Warning

In the preceding example, certificate passwords are stored in plain-text in appsettings.json. The $CREDENTIAL_PLACEHOLDER$ token is used as a placeholder for each certificate’s password. To store certificate passwords securely in development environments, see Protect secrets in development. To store certificate passwords securely in production environments, see Azure Key Vault configuration provider. Development secrets shouldn’t be used for production or test.

Schema notes:

  • Endpoints names are case-insensitive. For example, HTTPS and Https are equivalent.
  • The Url parameter is required for each endpoint. The format for this parameter is the same as the top-level Urls configuration parameter except that it’s limited to a single value.
  • These endpoints replace those defined in the top-level Urls configuration rather than adding to them. Endpoints defined in code via Listen are cumulative with the endpoints defined in the configuration section.
  • The Certificate section is optional. If the Certificate section isn’t specified, the defaults defined in Certificates:Default are used. If no defaults are available, the development certificate is used. If there are no defaults and the development certificate isn’t present, the server throws an exception and fails to start.
  • The Certificate section supports multiple certificate sources.
  • Any number of endpoints may be defined in Configuration as long as they don’t cause port conflicts.

Certificate sources

Certificate nodes can be configured to load certificates from a number of sources:

  • Path and Password to load .pfx files.
  • PathKeyPath and Password to load .pem/.crt and .key files.
  • Subject and Store to load from the certificate store.

For example, the Certificates:Default certificate can be specified as:

JSONCopy

"Default": {
  "Subject": "<subject; required>",
  "Store": "<cert store; required>",
  "Location": "<location; defaults to CurrentUser>",
  "AllowInvalid": "<true or false; defaults to false>"
}

ConfigurationLoader

Configure(IConfiguration) returns a KestrelConfigurationLoader with an Endpoint(String, Action<EndpointConfiguration>) method that can be used to supplement a configured endpoint’s settings:

C#Copy

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    var kestrelSection = context.Configuration.GetSection("Kestrel");

    serverOptions.Configure(kestrelSection)
        .Endpoint("HTTPS", listenOptions =>
        {
            // ...
        });
});

KestrelServerOptions.ConfigurationLoader can be directly accessed to continue iterating on the existing loader, such as the one provided by WebApplicationBuilder.WebHost.

  • The configuration section for each endpoint is available on the options in the Endpoint method so that custom settings may be read.
  • Multiple configurations may be loaded by calling Configure(IConfiguration) again with another section. Only the last configuration is used, unless Load is explicitly called on prior instances. The metapackage doesn’t call Load so that its default configuration section may be replaced.
  • KestrelConfigurationLoader mirrors the Listen family of APIs from KestrelServerOptions as Endpoint overloads, so code and config endpoints may be configured in the same place. These overloads don’t use names and only consume default settings from configuration.

Change the defaults in code

ConfigureEndpointDefaults and ConfigureHttpsDefaults can be used to change default settings for ListenOptions and HttpsConnectionAdapterOptions, including overriding the default certificate specified in the prior scenario. ConfigureEndpointDefaults and ConfigureHttpsDefaults should be called before any endpoints are configured.

C#Copy

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.ConfigureEndpointDefaults(listenOptions =>
    {
        // ...
    });

    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        // ...
    });
});

Configure endpoints using Server Name Indication

Server Name Indication (SNI) can be used to host multiple domains on the same IP address and port. For SNI to function, the client sends the host name for the secure session to the server during the TLS handshake so that the server can provide the correct certificate. The client uses the furnished certificate for encrypted communication with the server during the secure session that follows the TLS handshake.

SNI can be configured in two ways:

  • Create an endpoint in code and select a certificate using the host name with the ServerCertificateSelector callback.
  • Configure a mapping between host names and HTTPS options in Configuration. For example, JSON in the appsettings.json file.

SNI with ServerCertificateSelector

Kestrel supports SNI via the ServerCertificateSelector callback. The callback is invoked once per connection to allow the app to inspect the host name and select the appropriate certificate:

C#Copy

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(5005, listenOptions =>
    {
        listenOptions.UseHttps(httpsOptions =>
        {
            var localhostCert = CertificateLoader.LoadFromStoreCert(
                "localhost", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var exampleCert = CertificateLoader.LoadFromStoreCert(
                "example.com", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var subExampleCert = CertificateLoader.LoadFromStoreCert(
                "sub.example.com", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var certs = new Dictionary<string, X509Certificate2>(
                StringComparer.OrdinalIgnoreCase)
            {
                ["localhost"] = localhostCert,
                ["example.com"] = exampleCert,
                ["sub.example.com"] = subExampleCert
            };

            httpsOptions.ServerCertificateSelector = (connectionContext, name) =>
            {
                if (name is not null && certs.TryGetValue(name, out var cert))
                {
                    return cert;
                }

                return exampleCert;
            };
        });
    });
});

SNI with ServerOptionsSelectionCallback

Kestrel supports additional dynamic TLS configuration via the ServerOptionsSelectionCallback callback. The callback is invoked once per connection to allow the app to inspect the host name and select the appropriate certificate and TLS configuration. Default certificates and ConfigureHttpsDefaults are not used with this callback.

C#Copy

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(5005, listenOptions =>
    {
        listenOptions.UseHttps(httpsOptions =>
        {
            var localhostCert = CertificateLoader.LoadFromStoreCert(
                "localhost", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var exampleCert = CertificateLoader.LoadFromStoreCert(
                "example.com", "My", StoreLocation.CurrentUser,
                allowInvalid: true);

            listenOptions.UseHttps((stream, clientHelloInfo, state, cancellationToken) =>
            {
                if (string.Equals(clientHelloInfo.ServerName, "localhost",
                    StringComparison.OrdinalIgnoreCase))
                {
                    return new ValueTask<SslServerAuthenticationOptions>(
                        new SslServerAuthenticationOptions
                        {
                            ServerCertificate = localhostCert,
                            // Different TLS requirements for this host
                            ClientCertificateRequired = true
                        });
                }

                return new ValueTask<SslServerAuthenticationOptions>(
                    new SslServerAuthenticationOptions
                    {
                        ServerCertificate = exampleCert
                    });
            }, state: null!);
        });
    });
});

SNI with TlsHandshakeCallbackOptions

Kestrel supports additional dynamic TLS configuration via the TlsHandshakeCallbackOptions.OnConnection callback. The callback is invoked once per connection to allow the app to inspect the host name and select the appropriate certificate, TLS configuration, and other server options. Default certificates and ConfigureHttpsDefaults are not used with this callback.

C#Copy

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(5005, listenOptions =>
    {
        listenOptions.UseHttps(httpsOptions =>
        {
            var localhostCert = CertificateLoader.LoadFromStoreCert(
                "localhost", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var exampleCert = CertificateLoader.LoadFromStoreCert(
                "example.com", "My", StoreLocation.CurrentUser,
                allowInvalid: true);

            listenOptions.UseHttps(new TlsHandshakeCallbackOptions
            {
                OnConnection = context =>
                {
                    if (string.Equals(context.ClientHelloInfo.ServerName, "localhost",
                        StringComparison.OrdinalIgnoreCase))
                    {
                        // Different TLS requirements for this host
                        context.AllowDelayedClientCertificateNegotation = true;

                        return new ValueTask<SslServerAuthenticationOptions>(
                            new SslServerAuthenticationOptions
                            {
                                ServerCertificate = localhostCert
                            });
                    }

                    return new ValueTask<SslServerAuthenticationOptions>(
                        new SslServerAuthenticationOptions
                        {
                            ServerCertificate = exampleCert
                        });
                }
            });
        });
    });
});

SNI in configuration

Kestrel supports SNI defined in configuration. An endpoint can be configured with an Sni object that contains a mapping between host names and HTTPS options. The connection host name is matched to the options and they are used for that connection.

The following configuration adds an endpoint named MySniEndpoint that uses SNI to select HTTPS options based on the host name:

JSONCopy

{
  "Kestrel": {
    "Endpoints": {
      "MySniEndpoint": {
        "Url": "https://*",
        "SslProtocols": ["Tls11", "Tls12"],
        "Sni": {
          "a.example.org": {
            "Protocols": "Http1AndHttp2",
            "SslProtocols": ["Tls11", "Tls12", "Tls13"],
            "Certificate": {
              "Subject": "<subject; required>",
              "Store": "<certificate store; required>",
            },
            "ClientCertificateMode" : "NoCertificate"
          },
          "*.example.org": {
            "Certificate": {
              "Path": "<path to .pfx file>",
              "Password": "$CREDENTIAL_PLACEHOLDER$"
            }
          },
          "*": {
            // At least one subproperty needs to exist per SNI section or it
            // cannot be discovered via IConfiguration
            "Protocols": "Http1",
          }
        }
      }
    },
    "Certificates": {
      "Default": {
        "Path": "<path to .pfx file>",
        "Password": "$CREDENTIAL_PLACEHOLDER$"
      }
    }
  }
}

 Warning

In the preceding example, certificate passwords are stored in plain-text in appsettings.json. The $CREDENTIAL_PLACEHOLDER$ token is used as a placeholder for each certificate’s password. To store certificate passwords securely in development environments, see Protect secrets in development. To store certificate passwords securely in production environments, see Azure Key Vault configuration provider. Development secrets shouldn’t be used for production or test.

HTTPS options that can be overridden by SNI:

The host name supports wildcard matching:

  • Exact match. For example, a.example.org matches a.example.org.
  • Wildcard prefix. If there are multiple wildcard matches then the longest pattern is chosen. For example, *.example.org matches b.example.org and c.example.org.
  • Full wildcard. * matches everything else, including clients that aren’t using SNI and don’t send a host name.

The matched SNI configuration is applied to the endpoint for the connection, overriding values on the endpoint. If a connection doesn’t match a configured SNI host name then the connection is refused.

SNI requirements

All websites must run on the same Kestrel instance. Kestrel doesn’t support sharing an IP address and port across multiple instances without a reverse proxy.

SSL/TLS Protocols

SSL Protocols are protocols used for encrypting and decrypting traffic between two peers, traditionally a client and a server.

C#Copy

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.SslProtocols = SslProtocols.Tls13;
    });
});

JSONCopy

{
  "Kestrel": {
    "Endpoints": {
      "MyHttpsEndpoint": {
        "Url": "https://localhost:5001",
        "SslProtocols": ["Tls12", "Tls13"],
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      }
    }
  }
}

 Warning

In the preceding example, the certificate password is stored in plain-text in appsettings.json. The $CREDENTIAL_PLACEHOLDER$ token is used as a placeholder for the certificate’s password. To store certificate passwords securely in development environments, see Protect secrets in development. To store certificate passwords securely in production environments, see Azure Key Vault configuration provider. Development secrets shouldn’t be used for production or test.

The default value, SslProtocols.None, causes Kestrel to use the operating system defaults to choose the best protocol. Unless you have a specific reason to select a protocol, use the default.

Client Certificates

ClientCertificateMode configures the client certificate requirements.

C#Copy

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
    });
});

JSONCopy

{
  "Kestrel": {
    "Endpoints": {
      "MyHttpsEndpoint": {
        "Url": "https://localhost:5001",
        "ClientCertificateMode": "AllowCertificate",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      }
    }
  }
}

 Warning

In the preceding example, the certificate password is stored in plain-text in appsettings.json. The $CREDENTIAL_PLACEHOLDER$ token is used as a placeholder for the certificate’s password. To store certificate passwords securely in development environments, see Protect secrets in development. To store certificate passwords securely in production environments, see Azure Key Vault configuration provider.

The default value is ClientCertificateMode.NoCertificate where Kestrel will not request or require a certificate from the client.

For more information, see Configure certificate authentication in ASP.NET Core.

Connection logging

Call UseConnectionLogging to emit Debug level logs for byte-level communication on a connection. Connection logging is helpful for troubleshooting problems in low-level communication, such as during TLS encryption and behind proxies. If UseConnectionLogging is placed before UseHttps, encrypted traffic is logged. If UseConnectionLogging is placed after UseHttps, decrypted traffic is logged. This is built-in Connection Middleware.

C#Copy

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseConnectionLogging();
    });
});

Bind to a TCP socket

The Listen method binds to a TCP socket, and an options lambda permits X.509 certificate configuration:

C#Copy

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.Listen(IPAddress.Loopback, 5000);
    serverOptions.Listen(IPAddress.Loopback, 5001, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");
    });
});

The example configures HTTPS for an endpoint with ListenOptions. Use the same API to configure other Kestrel settings for specific endpoints.

On Windows, self-signed certificates can be created using the New-SelfSignedCertificate PowerShell cmdlet. For an unsupported example, see UpdateIISExpressSSLForChrome.ps1.

On macOS, Linux, and Windows, certificates can be created using OpenSSL.

Bind to a Unix socket

Listen on a Unix socket with ListenUnixSocket for improved performance with Nginx, as shown in this example:

C#Copy

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.ListenUnixSocket("/tmp/kestrel-test.sock");
});
  • In the Nginx configuration file, set the server > location > proxy_pass entry to http://unix:/tmp/{KESTREL SOCKET}:/;{KESTREL SOCKET} is the name of the socket provided to ListenUnixSocket (for example, kestrel-test.sock in the preceding example).
  • Ensure that the socket is writeable by Nginx (for example, chmod go+w /tmp/kestrel-test.sock).

Port 0

When the port number 0 is specified, Kestrel dynamically binds to an available port. The following example shows how to determine which port Kestrel bound at runtime:

C#Copy

app.Run(async (context) =>
{
    var serverAddressFeature = context.Features.Get<IServerAddressesFeature>();

    if (serverAddressFeature is not null)
    {
        var listenAddresses = string.Join(", ", serverAddressFeature.Addresses);

        // ...
    }
});

Limitations

Configure endpoints with the following approaches:

  • UseUrls
  • --urls command-line argument
  • urls host configuration key
  • ASPNETCORE_URLS environment variable

These methods are useful for making code work with servers other than Kestrel. However, be aware of the following limitations:

  • HTTPS can’t be used with these approaches unless a default certificate is provided in the HTTPS endpoint configuration (for example, using KestrelServerOptions configuration or a configuration file as shown earlier in this article).
  • When both the Listen and UseUrls approaches are used simultaneously, the Listen endpoints override the UseUrls endpoints.

IIS endpoint configuration

When using IIS, the URL bindings for IIS override bindings are set by either Listen or UseUrls. For more information, see ASP.NET Core Module.

ListenOptions.Protocols

The Protocols property establishes the HTTP protocols (HttpProtocols) enabled on a connection endpoint or for the server. Assign a value to the Protocols property from the HttpProtocols enum.

HttpProtocols enum valueConnection protocol permitted
Http1HTTP/1.1 only. Can be used with or without TLS.
Http2HTTP/2 only. May be used without TLS only if the client supports a Prior Knowledge mode.
Http1AndHttp2HTTP/1.1 and HTTP/2. HTTP/2 requires the client to select HTTP/2 in the TLS Application-Layer Protocol Negotiation (ALPN) handshake; otherwise, the connection defaults to HTTP/1.1.

The default ListenOptions.Protocols value for any endpoint is HttpProtocols.Http1AndHttp2.

TLS restrictions for HTTP/2:

  • TLS version 1.2 or later
  • Renegotiation disabled
  • Compression disabled
  • Minimum ephemeral key exchange sizes:
    • Elliptic curve Diffie-Hellman (ECDHE) [RFC4492]: 224 bits minimum
    • Finite field Diffie-Hellman (DHE) [TLS12]: 2048 bits minimum
  • Cipher suite not prohibited.

TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 [TLS-ECDHE] with the P-256 elliptic curve [FIPS186] is supported by default.

The following example permits HTTP/1.1 and HTTP/2 connections on port 8000. Connections are secured by TLS with a supplied certificate:

C#Copy

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
    });
});

On Linux, CipherSuitesPolicy can be used to filter TLS handshakes on a per-connection basis:

C#Copy

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.OnAuthenticate = (context, sslOptions) =>
        {
            sslOptions.CipherSuitesPolicy = new CipherSuitesPolicy(
                new[]
                {
                    TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
                    TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
                    // ...
                });
        };
    });
});

Connection Middleware

Custom connection middleware can filter TLS handshakes on a per-connection basis for specific ciphers if necessary.

The following example throws NotSupportedException for any cipher algorithm that the app doesn’t support. Alternatively, define and compare ITlsHandshakeFeature.CipherAlgorithm to a list of acceptable cipher suites.

No encryption is used with a CipherAlgorithmType.Null cipher algorithm.

C#Copy

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");

        listenOptions.Use((context, next) =>
        {
            var tlsFeature = context.Features.Get<ITlsHandshakeFeature>()!;

            if (tlsFeature.CipherAlgorithm == CipherAlgorithmType.Null)
            {
                throw new NotSupportedException(
                    $"Prohibited cipher: {tlsFeature.CipherAlgorithm}");
            }

            return next();
        });
    });
});

Set the HTTP protocol from configuration

By default, Kestrel configuration is loaded from the Kestrel section. The following appsettings.json example establishes HTTP/1.1 as the default connection protocol for all endpoints:

JSONCopy

{
  "Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http1"
    }
  }
}

The following appsettings.json example establishes the HTTP/1.1 connection protocol for a specific endpoint:

JSONCopy

{
  "Kestrel": {
    "Endpoints": {
      "HttpsDefaultCert": {
        "Url": "https://localhost:5001",
        "Protocols": "Http1"
      }
    }
  }
}

Protocols specified in code override values set by configuration.

URL prefixes

When using UseUrls--urls command-line argument, urls host configuration key, or ASPNETCORE_URLS environment variable, the URL prefixes can be in any of the following formats.

Only HTTP URL prefixes are valid. Kestrel doesn’t support HTTPS when configuring URL bindings using UseUrls.

  • IPv4 address with port numberCopyhttp://65.55.39.10:80/ 0.0.0.0 is a special case that binds to all IPv4 addresses.
  • IPv6 address with port numberCopyhttp://[0:0:0:0:0:ffff:4137:270a]:80/ [::] is the IPv6 equivalent of IPv4 0.0.0.0.
  • Host name with port numberCopyhttp://contoso.com:80/ http://*:80/ Host names, *, and +, aren’t special. Anything not recognized as a valid IP address or localhost binds to all IPv4 and IPv6 IPs. To bind different host names to different ASP.NET Core apps on the same port, use HTTP.sys or a reverse proxy server. Reverse proxy server examples include IIS, Nginx, or Apache. WarningHosting in a reverse proxy configuration requires host filtering.
  • Host localhost name with port number or loopback IP with port numberCopyhttp://localhost:5000/ http://127.0.0.1:5000/ http://[::1]:5000/ When localhost is specified, Kestrel attempts to bind to both IPv4 and IPv6 loopback interfaces. If the requested port is in use by another service on either loopback interface, Kestrel fails to start. If either loopback interface is unavailable for any other reason (most commonly because IPv6 isn’t supported), Kestrel logs a warning.

转自:https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-6.0#sni-with-servercertificateselector

分类
未分类

滴滴开源 LogicFlow:专注流程可视化的前端框架

前言

LogicFlow 脱胎于滴滴技术团队在客服业务下的实践,是由智能中台—体验平台研发的一款流程可视化的前端框架,提供了一系列流程图交互、编辑所必需的功能和灵活的节点自定义、插件等拓展能力,方便我们快速在业务系统内满足类流程图编辑器的需求。目前,LogicFlow 已经在公司内外不同用户的流程配置需求中得到了验证。

背景

首先,智能中台—体验平台技术团队几乎支持了滴滴所有业务板块客服系统的诉求,面对多样性、逻辑变更快的业务场景,传统的面向场景编程成本高且周期长。因此我们建设了线上配置化的运营系统,让运营、产品同学能够通过画流程图的方式变更线上的业务逻辑,比如用户电话进线时的互动式语音应答、人工客服在处理用户进线时的标准作业流程、用户自助解决问题的 H5 页面配置系统等千人千面的应用场景。

其次,各业务系统虽然都需要应用流程可视化技术,但需求各不相同。有的对流程图的要求比较简单,图的数据格式也简单,而有的需要按照 BPMN 的规范来绘制流程图,对于定制化的要求较高。我们调研了市面上相关的框架 (BPMN.js、X6、Jsplumb、G6-editor),均存在不满足的场景,技术栈统一的成本很高。具体表现在:

  1. BMPN.js、Jsplumb 的拓展能力不足,自定义节点支持成本很高;只能全量引入,各系统无法按需引入
  2. 与后端配套的流程引擎适配,成本较高。均不支持数据转换、不支持流程的校验等业务定制需求。
  3. 文档、示例不健全。X6 和 BPMN 的文档不健全,示例少(2020 初调研结论)

因此,我们在 2020 上半年开启了 LogicFlow 的项目,支持各系统的流程可视化需求。

LogicFlow 的能力和特性

LogicFlow 当前已具备了哪些能力呢,我会分两部分来介绍。

快速搭建流程图编辑器

提供了一个流程图编辑所必需的各项能力,这也是 LogicFlow 的基础能力:

  • 图的绘制能力。基于 SVG 来绘制形状各异的节点和线,并提供了基础的节点(矩形、圆形、多边形等)和线(直线、折线、曲线)
  • 各类交互能力,让图动起来。根据节点、线、图的各类鼠标事件(hover、点击、拖拽等)做出反应。比如节点拖拽、拖拽创建连线、线的调整、双击节点编辑文本等
  • 提升编辑效率的能力。提供网格、对齐线,上一步、下一步,键盘快捷键,图放大缩小等配套能力,帮助用户提升编辑效率
  • 提供了丰富的 API ,宿主研发通过 API 传参调用和监听事件的方式,与 LogicFlow 完成交互

通过以上能力,前端研发可以低成本、快速的搭建起流程可视化的应用,提供流畅的产品交互。下面是通过 LogicFlow 内置的节点和配套能力,做的流程图示例:

example1

基于业务场景拓展

当基础能力无法满足业务需求的时候,便需要基于业务场景拓展。这也是 LogicFlow 能支持客服侧多个系统的关键所在。

  • 设置图上所有元素的样式,比如各种节点、线、锚点、箭头、对齐线的大小颜色等,满足对前端样式调整的需求
  • API 拓展。支持在 LogicFlow 上注册自定义的方法,比如通过 API 拓展提供图片下载的方法
  • 自定义节点、线。内置的矩形、圆形等图形类节点往往无法满足实际的业务需求,需要定义具有业务意义的节点。LogicFlow 提供了 的方式让用户定制具有自定义图形、业务数据的节点,比如流程审批场景中的 “审批” 节点
  • 拓展组件。LogicFlow 在 SVG 图层上提供了 HTML 层和一系列坐标转换逻辑,并支持在 HTML 层注册组件。宿主研发可以通过 LogicFlow 的 API,基于任何 View 框架开发组件,比如节点的右键菜单、控制面板等
  • 数据转换 adapter。LogicFlow 默认导出的图数据不一定适合所有业务,此时可以通过 adapter API,在图数据从 LogicFlow 输入、输出的时候做自定义转换,比如转换成 BPMN 规范的图数据
  • 内置部分拓展能力。基于上述拓展能力,我们还单独提供了 lf-extension 的包,用来存放客服业务下沉淀出的具有通用性的节点、组件等,比如面向 BPMN 规范的节点和数据 adapter,默认菜单。注意 lf-extension 可以单独安装,并支持按需引入

基于上述拓展的能力,前端研发能够根据实际业务场景的需求,灵活的开发出所需的节点、组件等。下面有两个基于 LogicFlow 拓展能力做出的流程图:

BPMN:

图片:bpmn

审批流程:

图片: 审批流

定位对比

dingwei1

上图是通过横纵两个维度来对比目前大家耳熟能详的几个开源框架,以了解 LogicFlow 的定位。横轴是该框架在图可视化能力的丰富程度,纵轴越靠上则代表这个框架在业务流程应用上的成熟度越高,初次部署的开发成本越低。 让我们分别来介绍一下这几个框架:

  • activiti 作为工作流引擎提供了前后端的解决方案,简单二次开发就可以部署一套业务流程的管理平台
  • Bpmn.js:基于 BPMN2.0 规范,设计的流程图编辑器
  • G6:antv 旗下专注图形可视化,各类分析类图表。比如生态树、脑图、辐射图、缩进图等等
  • X6:图编辑引擎,核心能力是节点、连线和画布。不仅支持了流程图,还有 Dag 图、ER 图

LogicFlow 的定位在上图的 Bpmn.js 和 X6 之间,填补中间的空白。核心提供了流程图的编辑器,并且通过拓展能力来支持 BPMN 等规范所需的流程节点和数据格式,以满足当前业务下的现状。

实现原理和架构

整体架构图

图片: lfjk

核心包 @logicflow/core 提供了流程图编辑器基础的能力,右边的 @logicflow/extension 是基于 @logicflow/core 的拓展性开发的插件。

流程图编辑器的设计方案

主要介绍一下实现流程图编辑器重要的选型和方案设计。

图渲染方案

前端绘制图形无非就是 HTML + CSS、Canvas、Svg 三种方式,我们综合做了一下对比,列出了相应的优劣势:

bijiao1

在流程图的场景下,不需要渲染大量的节点(最多几千个元素),对于动画的诉求也不高。Svg 基于 DOM 的特性会更适合我们,一个是学习成本和开发成本更低,另一个是基于 DOM 可以做的拓展也更多。不过 Svg 标签内部并不支持插入其他比如 div 这种标签,所以在实现某些功能的时候,都需要结合其他 HTML 标签。

所以最终我们选择使用 HTML + Svg 来完成图的渲染,Svg 负责图形、线的部分,HTML 来实现文本、菜单、背景等图层

模块抽象

基于上述方案,下一步我们要做的是对实现一张流程图做分类和抽象。

图片: mkcx

通过上图:

  • 首先我们构建了多个图层来承担不同的职责,以方便实现功能和能力拓展。最上层的是 Svg 图层,所有图形(节点、线、对齐线、outLine 等)均在 Svg 上渲染,也负责监听图上的各种事件。Svg 下层的分别是组件层,负责拓展 UI 组件;Grid 层,负责渲染网格;背景层,添加自定义的背景。
  • Shape 的职责主要是基于 Svg 对图形渲染的封装,提供默认样式、把用户传入的属性做转换等,主要包含 Rect、Circle、Ellipse、Polygon、Path、PolyLine、Text 等,方便 LogicFlow 内部复用,比如圆形节点和锚点都需要 Circle。
  • 基于 Shape,还实现了很多小元素,比如节点和线需要的锚点,比如线上的箭头等等。
  • 而 BaseNode、BaseEdge 则是节点和线通用能力的封装,聚合 shape、锚点、文本,还封装了对事件和样式的处理等。通过继承 BaseNode,传入 shape 我们可以得到 RectNode、CircleNode 等可渲染的节点。

因为流程图是富交互或者说是重编辑的,有了这几个基础的模块,接下来要做的就是富交互的方案设计,即用户在图上做的任何操作都要给出响应。比如我触发一个节点的拖拽,那关联的线可能需要跟着动,还能识别出在某个水平线上有没有其他节点(对齐线)。

MVVM + Virtual DOM

首先我们考虑到整个图编辑器具备很多状态存储,并且要实现编辑图上各模块的响应就必须要有状态的通信能力。第二如果要实现类似 redo/undo 这类功能,那整个图就一定需要根据数据得出渲染,即 fn(state) => View ,比较好的方式就是通过 Model 来驱动 View。

最终我们选择基于 MVVM,这个广泛被应用于当前前端工程中的设计模式来构建 LogicFlow 的图编辑器,定义图的 View 和 Model 层,使工程代码具备一定的解耦。与此同时,引入 Mobx 来实现我们的状态管理、数据响应的能力,一张图基于一份 Model 做状态的通信。此外,考虑 Mobx 的另一个原因是:只要我想,那就可以做到最细颗粒度的数据绑定(观测),可以减少没必要的渲染。

以下是 LogicFlow 图编辑器的 MVVM 示意图: 图片: mvvm

通过上图可以看到,View 层(图、节点等)通过数据绑定,会在 Model 发生变化之后做出响应/更新。前面我们提到了关于图的渲染我们是基于 Svg + HTML 实现的,那要做 View 层的更新无非就是命令式和声明式两个选择:

  • 命令式。比如 jQuery 的 api,$('.rectNode').attrs({x: 1, y: 2}),像这种方式操作 DOM 代码其实比较繁琐,在重交互的场景下写的代码会比较冗余。虽然我们最终找到了有一个库能够很方便的支持通过命令式的方式来绘图 —— antv/g
  • 声明式。比如 React/Vue 这类 View 框架,其中一个比较核心的能力就是做到了 state => UI ,通过声明式的方式来构建 DOM,只要状态发生变化,那 UI 就更新

除了考虑到命令式在操作 DOM 的场景下写代码会比较繁琐之外,还有一个原因就是操作 DOM 的成本问题,在基于 State 更新 UI 的设计下,我们自然而然想到了引入 Virtual DOM 来解决某些场景下的更新效率,这也可以一定程度上弥补「基于 Svg 渲染图形」可能造成的渲染性能问题。

总之,选择 MVVM 的设计模式并引入 Virtual DOM,最根本的两个原因便是提升我们图编辑器场景下的开发效率,以及在 HTML + Svg 的图渲染方案下,可以追求更好的性能表现

我们与 X6 做了一次渲染时的性能比较,在相同的运行环境下,分别测出 LogicFlow 和 X6 在不同量级的节点/线下,渲染出流程图的时间,理论上渲染时间越短,性能表现越好。

对比1
对比2

通过上述表格,我们测算出 LogicFlow 在初始渲染速度上是优于 X6 的,并且这还没有开启LogicFlow 的按需加载功能,也验证了我们的技术选型。你也可以在示例页进行测试: yhlchao.github.io/LF-VS-Other…

事件系统

介绍了在 “状态” 和 “响应” 我们做的设计,那要收集到用户的各类 “操作” 并及时上报和冒泡,就需要一套事件系统。最主要的就是复用和统一上报。

图片: Event

复用即怎么保证所有节点和线都能具备默认的事件回调,以及针对复杂事件(拖拽)的处理逻辑如何共用。

  • Behavior。针对复杂事件的处理,我们做了 function 和 class 形式的封装,比如 Drag 是通过 mousemove、down、up 来模拟 h5 的 dragEnter、dragOver、dragEnd 和 drop 事件,DnD 则是通过抽象 dragsource 和 droptarget 两个实体来实现 drag 和 drop 的交互,比如拖拽创建节点
  • 在前文模块抽象章节提到了内部有 BaseNode 和 BaseEdge 这样的抽象,内置节点和自定义的节点都通过继承基类来获得通用的能力,所以 LogicFlow 内部默认的事件回调实际是通过继承来复用的
  • EventCenter。通过事件总线做统一上报,把内部捕获到的所有用户行为事件,按照一定的规范和格式emit(ev, args)都上报到 EventCenter,最终冒泡到 LogicFlow 类,由 LogicFlow 类统一跟宿主交互。此外,图编辑器内任何地方也都可以通过 EventCenter 做事件的触发和监听

工具中心

工具中心的定位是解决某类特定问题的 utils,比如上面提到的 Behavior(复杂事件的封装) 和 EventCenter。此外,在图编辑的过程中,如果要实现比较好的交互效果,实际有很多复杂的计算逻辑要处理。

  • 坐标系。浏览器的 clientX、clientY 坐标系,以及 Svg 图本身的坐标系,当出现图的缩放和平移的时候,两个坐标系显然是不同的,那如何做坐标系的转换。
  • Algorithm。是专门通过几何、算法来处理可视化的一些问题。比如:当一个节点在同一方向有多条折线连出的时候,如何做路径的合并以展示起来更美观,如下 图片: shili1 如何计算出一根线到一个图形的切点,以达到线可以连接图形非锚点的位置,如下图 图片:shili2
  • History,主要提供 redo 和 undo 的能力。通过两个栈来存储 undos 和 redos,并限制最大长度,得益于 MVVM 的设计模式,能方便的做数据变化的观测和 Model 驱动 View。

可扩展性

介绍完流程图编辑器的设计方案,现在来介绍 LogicFlow 的另一个重要特性,关于拓展性方面的设计。在程序世界中,小到一个 function,一个服务,再到一个开发框架 react,小程序开发框架,大到一个 Chrome 类的应用平台,都具备自己的可扩展性,这也是软件发展过程中要考虑的一种设计选择。对于 LogicFlow,是解决某个领域问题的开发框架,首先 API 要具备可扩展性;此外 LogicFlow 还提供了视图层,在 View 部分应该能够让用户做二次开发。这两个扩展的方向确定之后,最主要的还是结合业务需求,要能满足当前和未来一段时间内预见的业务场景,但也不能过度设计。

API 上的设计

首先,LogicFlow 在面向用户使用这一层,完全是基于面向对象的设计模式封装的,最大的好处是几乎每个程序员都熟悉它的使用,使用成本低。通过下面初始化方式便可以了解。

js

复制代码const lf = new LogicFlow({ // 实例化 lf 对象 container: document.querySelector('#graph'), // 获取渲染容器 width: 700, height: 600, tool: { menu: true, control: true, }, background: { color: '#F0F0F0' }, grid: { type: 'dot', size: 20, }, }); lf.render({ nodes: [], edges: []}); // 在界面上渲染视图

通过 class LogicFlow,用户实例化一次便得到一个流程图的实例,状态也是私有的,各种使用方法通过 lf 的实例调用即可。 关于 API 拓展的设计总结来看:

  1. 面向对象的设计模式, LogicFlow 内部做好封装,用户可以做继承、重写接口/方法
  2. 方法的设计。首先是要有固定类型的输入和输出。此外,LogicFlow 也提供了类似于 extends 的方法,通过 LogicFlow.use(fn) 在原型上拓展方法
  3. 通过观察者的模式做通信,即提供 on 方法供宿主订阅各类内部事件
  4. 图的数据可定制。无论是一个节点、线有哪些自定义的业务属性,还是流程图要导出什么样的数据,都应该能够定制。

插件化

View 层的拓展性,除了用户能够定制展现方式之外,最重要的是插件化,因为在流程可视化这条路上,不同的业务场景下需要的能力不尽相同,LogicFlow 很难做到支持所有的场景,所以提供好的插拔能力,让用户二次开发是比较好的选择。目前,在 UI 界面上,我们开放了两个能力:

  1. 节点和线支持二次开发,即自定义节点、线
  2. 可开发 UI 组件注册到 LogicFlow 的组件画布内

基于插件化的思路,我们已经支持了不同的业务系统,并在这个过程中把一些稍微通用的能力沉淀出来,并封装到 lf-extension 包,比如用来支持 BPMN 规范的节点。目前 extension 内的拓展主要分了四类:UI 组件、自定义节点、API、adapter。

未来规划

  1. API 的易用性和丰富程度。具体的功能 scope 除了我们当前的迭代计划(详见 github 仓库的 project),还会根据用户的需求排出优先级后加入进来,也希望大家多多提意见和需求。这个方向的基调是保持 LogicFlow 流程可视化的定位,把 core 的 API 丰富,extension 的能力增强
  2. 更完善的文档和示例。主要是文档易读、完善,能够有完整的示例和代码,供开发者 copy paste 代码,目前示例只有 react 版,2021.4 之前会增加 vue 版的示例
  3. 不仅是流程可视化库,期望提供整套解决方案。LogicFlow 只解决了前端流程图编辑的技术问题,但关于图数据的定义,流程最终如何被执行,还需要一个配套的流程引擎。目前,关于「流程引擎」我们团队也有相应的解决方案 —— turbo(Java 版已开源:github.com/didi/turbo) 我们会把 LogicFlow 和 turbo 做成端到端的解决方案,并提供完整的应用示例。此外,Nodejs 版的引擎也在规划中,大家拭目以待。

最后

相信你对 LogicFlow 已经有一个大概的认识了,如果在你负责的业务中也有流程可视化的诉求,并且有较高的拓展性需求,那 LogicFlow 会是一个好的选择。对于 LogicFlow 技术本身的实现细节、对于相似业务的探讨也都欢迎大家来交流。我们后续会有更多的文章介绍 LogicFlow 在技术设计细节以及我们对于可视化、业务流程、逻辑编排等领域的一些思考,尽情期待。

作者:滴滴前端技术
链接:https://juejin.cn/post/6933413834682007560
来源:稀土掘金