1. 如何使用低代码,开发合同管理系统

本教程指导您如何使用华炎魔方,创建合同管理应用,并同步到代码,通过编写触发器实现高级业务逻辑。

华炎魔方基于商业智能和模型驱动,帮助您轻松便捷地创建智能化、移动化的企业应用。华炎魔方,最适合的场景是,技术人员以此为基础进行企业业务应用系统的二次开发。

首先,需要在本地的Linux/Mac/Windows环境中安装华炎魔方低代码开放平台,以及其他一些必要的开发工具。更为简化的方式,是在线申请开通华炎魔方云服务 。

安装好华炎魔方后,就可以以此为基础进行应用开发了。这个过程包括:

  • 可视化开发,在页面上直接配置对象(表)与应用
  • 通过同步工具,将配置的对象与应用转成代码
  • 基于代码进行进一步的开发,增加处理业务逻辑的API接口、触发器、按钮等

我们以企业应用中常见的合同管理系统为例,一起了解如何在华炎魔方里进行配置与开发。

可视化开发

在华炎魔方中构建应用程序时,可以使用可视化界面管理所有的元数据。开发的第一步,就是由管理员在“设置”里,对系统进行管理,并对对象、应用等元数据进行配置。

比如合同管理,主要的对象就是合同,我们首先来配置它。

创建合同对象

管理员,在设置》对象设置》对象,

点击新建按钮,输入合同对象的显示名、API名称等,点击保存按钮,即新建对象。

新建对象字段

对象新建时,系统会自动给对象增加4个对象字段,而其他的字段可自行定义。

比如,我们设想的合同对象,准备设置如下字段:

序号字段名字段API类型
1名称name文本
2签订日期signed_date日期
3合同金额amount数值
4收支类别bop选择框
5签约对象othercompany相关表(内置对象:业务伙伴)
6合同编号no文本
7款项已结清paid_all选择框
8合同主要内容subject长文本

我们在对象的详情页面上,

点对象字段列表左上角的“新建”按钮,逐一新建所需字段。

新建和修改字段时,可以配置排序号、宽字段、必填等高级选项。

字段属性填写完毕,点击“提交”。

其他字段的建立方法也一样

修改列表视图

在对象新建的时候,系统还会自动新建2个列表视图,

  • 所有(all)
  • 最近查看(rescent)

可自行修改其设置。点击“编辑”,可以按先后顺序,增加列表视图上所要显示的字段,

显示的列调整完毕,点击“提交”。

预览对象

已配置的对象,可以通过对象的“预览”按钮,查看对象的列表显示、新增、详情显示等情况。

点击对象详情页的“预览”,就进入了对象的列表显示页,点击“新建“按钮

就可以新建合同了。也可以试着用下显示、编辑、删除、导出等功能。

创建合同的子表

在合同管理中,除了合同,我们还需要管理合同的付款等。在华炎魔方里,合同是一个对象,相当于表;付款则是另一个对象,相当于另一张表。而付款与合同存在密切的逻辑关系。

主表与子表

同一份合同,可能会有多笔付款,这里,合同是主表,付款则是子表。

在华炎魔方中,合同(主表)是一个对象,付款(子表)则是另一个对象,需要分别定义。同时,可以定义一个“主表/子表”字段来描述合同与付款两个对象之间的主子表关系。

新建子对象:付款

下面,我们新建合同对象的子对象:付款。

管理员,进入设置》对象设置》对象,点击新建按钮,输入显示名、API名称等,点击保存按钮,即新建对象。

新建对象字段

对象新建后,我们继续设定其他的字段。

按预定目标,付款对象,拟设置如下字段:

序号字段名字段API类型
1相关合同contract主表/子表(合同)
2计划付款日期due_date日期
3付款金额amount数值
4付款状态payment_status选择框
5付款日期payment_date日期
6付款说明payment_note长文本

其中的“相关合同“这个字段,就实现了合同与付款之间的主子表关系 。

在付款对象的详情页面上,点“新建”按钮逐一新建上述字段。

修改列表视图

付款对象新建时,系统同样会自动新建2个列表视图。我们可以按先后顺序增加列表视图显示的列字段。

预览对象

已配置的付款对象,同样可以通过对象的“预览”按钮,查看列表显示、新增、详情显示等情况。

如果我们预览合同主表的记录,可以看到会多出付款的子表记录。

创建合同应用

建立好合同、付款等2个对象后,我们可以建立自定义应用:合同了。

新建应用:合同

管理员,进入 设置》对象设置》对象,点击新建按钮,

输入应用程序的名称、API名称等,选择好桌面主菜单、手机主菜单,点击保存按钮。

试用合同应用

建立自定义应用后,就可以进入应用,查看这个应用的具体情况了。

进入应用:合同

点击左上角的“应用程序启动器”图标,可以点击进入合同应用。

里面已经有之前在预览时录入的数据。

查看合同详情页,不但显示合同的详情,也会将作为子表的付款记录列表显示。

经过上述的配置,我们就建立起了合同管理系统的框架。具备了合同应用的基本功能,比如管理合同,建立和跟踪合同的付款记录等。

同步代码

在界面配置好相应的对象及应用后,可以将这些元数据通过同步工具转换为代码,为后续的代码扩充作准备。

首先需要安装华炎魔方同步工具,安装方法和过程如下:

在VS Code中安装插件

在Visual Studio Code中,进入 扩展页面,搜索“Steedos”,安装 “Steedos Extensions for Visual Studio Code”插件

安装后,可见“Steedos CLI Integration”也已安装好。

修改配置文件

修改根目录下的 .env,增加以下两个参数,来实现元数据同步功能,其中METADATA_SERVER 为系统的Root的URL,METADATA_APIKEY为激活本地私有化华炎魔方自动生成的的API Key。

[metadata]
METADATA_SERVER=http://127.0.0.1:5000
METADATA_APIKEY=-D0hUDsU0-_nhonh8TKZRukDZsqQQwiLCy

重启服务

修改配置文件后,需重启华炎魔方服务。

重启后,在 VS Code中,进入Steedos插件页,可以看到自定义的对象及应用等。

从数据库同步到代码

在 VS Code中,切换到Steedos插件,可以看到已在页面配置的元数据,包括对象、应用等

将页面配置的对象同步为代码

点击 自定义对象 后的 下载图标,

即可将页面配置的对象转换为对象代码。

生成的对象代码

主要包括:

  • ***.object.yml ,对象的基本配置属性

  • fields文件夹,其下是每个字段对应的文件 ***.fileld.yml

  • listviews文件夹,其下是每个列表视图对应的文件 ***.listview.yml

将页面配置的应用同步为代码

点击 自定义应用 后的 下载图标,

即可将页面配置的应用转换为应用代码。

生成的应用代码

应用代码是 ***.app.yml ,应用的基本配置属性。

代码开发

在上一节,我们已将页面配置的对象及应用转为了代码,下面,我们就可以通过代码来扩充业务逻辑了。

例如,在合同管理的需求中,如果合同的付款已结清,就需要将合同归档,不再开放编辑功能,也不再允许录入付款记录。在华炎魔方里,可以通过对象记录的“锁定”来实现,即将记录的字段locked赋值为 true 。

为此,我们需要做:

  1. 建立自定义按钮:归档,点击后调取后台的合同归档接口
  2. 建立自定义服务端API:合同归档,实现合同归档的业务逻辑

下面,我们分别来关注下 服务端API 和 按钮。

自定义服务端API

我们首先来建立自定义服务端API 。

华炎魔方已经内置了自定义对象的增、删、改等基本操作的API,如需进行业务逻辑扩充,推荐自定义API接口,用于接收数据、处理业务逻辑、返回结果及数据,包括:

  • 从本系统的数据库表中,取得所需的相关表的数据;
  • 增加、修改、删除本系统的相关库表记录的特定字段值;
  • 调用第三方系统接口,传入所需数据、执行特定处理;
  • 调用第三方系统接口,读取数据,并存入本系统的数据库中。

API接口定义完成后,可调用API接口的包括:

  • 本系统的自定义按钮
  • 本系统的触发器
  • 第三方系统

下面,我们以合同归档的需求为例,介绍下如何创建API 。华炎魔方中,服务端API即为Router文件,由Javascript编写。

创建Router

我们先在 VS Code中,打开查看下的“命令面板”,输入Steedos,选择 “Steedos:Create Router”,创建Router文件

输入文件名,例如contract_locked,即可在相应路径下创建Router文件。

Router的默认内容

contract_locked.router.js

const express = require("express");
const router = express.Router();
const core = require('@steedos/core');

const objectql = require('@steedos/objectql');

router.get('/api/test', core.requireAuthentication, async function (req, res) {
    // const userSession = req.user;
    // const spaceId = userSession.spaceId;
    // const userId = userSession.userId;
    // const isSpaceAdmin = userSession.is_space_admin;
    
    res.status(200).send({ message: 'router ok' });

});
exports.default = router;

修改Router,增加业务逻辑内容

接下来,我们在Router文件中继续编写业务逻辑。

contract_locked.router.js

const express = require("express");
const router = express.Router();
const core = require('@steedos/core');

const objectql = require('@steedos/objectql');

router.post('/api/contract/locked/:contractId', core.requireAuthentication, async function (req, res) {
    // const userSession = req.user;
    // const spaceId = userSession.spaceId;
    // const userId = userSession.userId;
    // const isSpaceAdmin = userSession.is_space_admin;

    const { contractId } = req.params;
    const contract = await objectql.getObject("contracts").updateOne(contractId, {
        locked: true
    });
    
    res.status(200).send({ message: '本合同已归档', success:true });

});
exports.default = router;

上面,就是 通过调用objectql.getObject().updateOne()来给合同记录的locked赋值。

关于如何查询、插入、修改、删除华炎魔方的内置/自定义对象,可参考官网相关介绍。

自定义按钮

合同归档的需求,是在前台页面存在一个“归档”按钮。因为系统自带的默认按钮只有新增、修改等,我们需要自定义这个按钮。

创建自定义按钮相关文件

在 VS Code中查看代码,在合同对象下建立文件夹“buttons”,右键后,选择“Steedos:Create Object Button”

输入按钮名,例如close,即可在buttons下创建按钮相关文件。

Button的默认内容

按钮文件包括:

  • ***.button.yml ,声明操作按钮
  • ***.button.js ,定义操作按钮的工作、及控制是否显示按钮

close.button.yml

name: close
is_enable: true
label: close
'on': record_only
visible: true

close.button.js

module.exports = {
    close: function (object_name, record_id) {
        
    },
    closeVisible: function () {
        return true
    }
}

修改Button,增加处理内容

按预想要求,我们修改按钮文件的内容。

close.button.xml

name: close
is_enable: true
label: 归档
'on': record_only
visible: true

close.button.js

module.exports = {
    close: function (object_name, record_id) {
        let url = `api/contract/locked/${record_id}`;
        let options = { type: 'post', async: false };
        let result = Steedos.authRequest(url, options);
        console.log(result);
        if (result && result.success == false) {
            toastr.error(result.message);
        } else if (result && result.success) {
            toastr.success('归档成功。');
            FlowRouter.reload();
        }
    },
    closeVisible: function (object_name, record_id, record_permissions, record) {
        if (record.locked == true) {
            return false
        } else {
            return true
        }
    }
}

这里,在close函数里,调用自定义服务端API“api/contract/locked/” ;在closeVisible函数里,按locked的值判断是否显示“归档”按钮。

将代码同步发布到页面配置

自定义按钮需发布到页面,才能测试确认。

在 VS Code的代码中,右键合同对象,选择“Steedos: Deploy Source”。

执行后,可以看到右侧输出提示`deploy success!`,一般来说不需要重启服务即可生效。

  • 请注意只有可以在界面维护的元数据才支持用vscode同步插件来同步界面和vscode代码之间的元数据,目前有一些代码级的元数据是不支持同步的,比如上面提到的触发器、路由就不支持,这些元数据只支持用代码来维护。
  • 一般来说支持用vscode同步插件来同步的元数据在界面上修改后立即就能生效,在vscode上修改后再执行deploy同步操作后也能生效,它们都不需要重启服务就能生效。
  • 建议界面上维护的元数据都尽快用vscode同步插件同步为代码,方便后续代码维护,也只有这样才能把相关元数据部署到其他数据库。
每次执行deploy前应该先执行`Steedos: Retrieve Source`把数据库中其他协同工作的人在界面上处理的元数据先下载到本地,以避免把其他人在界面上做的元数据操作覆盖了。

测试确认

进入华炎魔方系统,在合同详情页,出现了“归档”按钮

点击此按钮

可以看到此合同已归档(无法编辑)了。

触发器

在上面的两节里,我们看到了如何自定义API和按钮,下面一起看到触发器(Trigger)的部分。

我们的合同管理需求里,假设包括:

  • 增加付款记录时,增加判断:如果合同已被归档,则禁止这个增加付款的操作。
  • 修改付款记录后,增加判断:如果全部款项已付款,则标记合同为已完成付款。

上述的需求即可通过触发器实现,相关文件名称以.trigger.js结尾。下面我们给付款对象建立这2个触发器(Trigger)。

创建Trigger

在 VS Code中,打开查看下的“命令面板”,输入Steedos,选择 “Steedos:Create Object Trigger”

选择所需的触发器,这里选择“beforeInsert”、“afterUpdate”

即可在相应路径,创建Trigger文件。

Trigger的默认内容

pay.trigger.js

const objectql = require('@steedos/objectql');

module.exports = {
    listenTo: 'Your object name',

    beforeInsert: async function(){
    
    },

    afterUpdate: async function(){
    
    },

}

其中listenTo就是设定触发器绑定的对象,本文,这就应修改为“payment__c” 。

修改Trigger,增加beforeInsert触发器

需求是,在增加付款记录时,判断如果合同已归档,则禁止增加付款记录。

pay.trigger.js

const objectql = require('@steedos/objectql');

module.exports = {
    listenTo: 'payment__c',

    beforeInsert: async function(){
        let doc = this.doc;
        let contractId = doc.contract__c;
        console.log("contractId:",contractId);
        if (contractId) {
            let contractObj = this.getObject('contract__c');
            let contractDoc = await contractObj.findOne(contractId, { fields: ['locked'] });
            console.log("contractDoc",contractDoc);
            console.log("contractDoc.locked",contractDoc.locked);
            if (contractDoc && contractDoc.locked == true) { 
                throw new Error('合同已归档,不能再增加付款记录。');
            }
        }  
    },

    afterUpdate: async function(){
    
    },

}

在trigger的beforeInsert里,调用this.getObject(‘contract__c’).findOne()取得付款对应的合同的属性,如果locked,则抛出Error。

重启服务后,即可在前台进行测试确认。

在尚未归档前,可以“新建”付款,正巧,这时别的用户已将此合同归档,所以提交后,后台检测到合同已归档,发出报错信息、本次新增操作未生效。

修改Trigger,增加afterUpdate触发器

需求是,修改付款记录后,判断如果全部款项已付款,则标记合同为已完成付款。

const objectql = require('@steedos/objectql');

module.exports = {
    listenTo: 'payment__c',

    beforeInsert: async function(){
        let doc = this.doc;
        let contractId = doc.contract__c;
        console.log("contractId:",contractId);
        if (contractId) {
            let contractObj = this.getObject('contract__c');
            let contractDoc = await contractObj.findOne(contractId, { fields: ['locked'] });
            console.log("contractDoc",contractDoc);
            console.log("contractDoc.locked",contractDoc.locked);
            if (contractDoc && contractDoc.locked == true) { 
                throw new Error('合同已归档,不能再增加付款记录。');
            }
        }  
    },

    afterUpdate: async function(){
        let doc = this.doc;
        const contractId = doc.contract__c;
        const contractObj = this.getObject('contract__c');
        const paymentDoc = this.getObject('payment__c');        
        let updateFlag = true;

        const paymentDocs = await paymentDoc.find({ filters: [['contract__c', '=', contractId]], fields: ['_id','payment_status__c','amount__c'] });
        for (const pDoc of paymentDocs) {
            if (pDoc.payment_status__c == 'unpaid'){
                updateFlag =  false;
            }
        }

        if (updateFlag === true){
            await contractObj.update(contractId, { paid_all: true });       
        }
    },

}

在trigger的afterUpdate里,调用this.getObject(‘paymentc’).find()取得付款对应的合同的所有付款记录,循环判断,如果所有记录的payment_statusc都不是’unpaid’,则调用this.getObject(‘contract__c’).update()给合同的字段“已完成付款”赋值为true 。

重启服务后,即可在前台进行测试确认。修改某合同最后一条付款记录

修改为已支付后,

可以看到这个合同,已被标记为款项已结清 。