华冠智能制造平台是以Lumos为基础,有多个应用共同运行其上的一个完善的系统,只要依照其开发规则,即可实现定制化开发。 以下内容将针对二次开发中使用的技术及模块展开讨论。

1. 先决条件

Artemis使用的镜像版本为2.11.0

1. 华冠智能制造平台二次开发文档

1.1. 名词介绍

Abbreviation Comment

LSH

Lumos Service Hub,特指 Lumos 后台服务,完成特定的功能,不需要页面交互,典型的使用场景有:数据监控、第三方系统数据同步、测试文件分析等,该服务可以作为 Windows 上的系统服务或 Linux 上的系统服务存在,开机启动。

1.2. 软件架构介绍

1.2.1. 平台优势

  • 基于网页B/S架构设计,升级方便,使用简单。

  • 支持多语言。

  • 拖拽式的工艺路线设计,可视化的建模场景,从具体到抽象,从繁琐到简单。

  • 基于云架构的应用设计,采用私有云+公有云的部署方式,将传统与现代相结合,生产中赋予智慧。

  • 平台化产品,对建模的对象,如物料等,支持属性扩展,以满足不同行业的发展需要。

  • 支持报表扩展。用户可通过报表工具自主研发报表,并与平台无缝集成。

  • 以配置驱动功能的扩展,避免大量的二次开发工作。

  • 模块独立,可按照实际要求逐步实施,满足中小企业灵活多变的业务需求。

  • 与众多软件提供商集,可适配市面上大部分硬件,为生产智能化打下基础。

  • 原生报表丰富,从设计到生产看板,正追踪,反追溯等个用户对于生产流程的把控细致入微

1.2.2. 平台逻辑架构图

..\images\core\architecture

整体分为3层架构,分别为逻辑处理层,中间适配(sdk)层,前端表现层。

逻辑处理层

所有跟业务逻辑的处理会全部放在这一层,包含以下几个方面:

  • 表结构的设计,数据持久化

  • Entity对象的设计

  • 业务逻辑处理,专注于业务逻辑的实现,是Lumos平台的核心。在MES中,如过站逻辑,质量卡控,工艺路线定制等的处理。

  • 权限的卡控

  • RPC接口的发布。平台的所有接口,会以Dubbo技术对外发布RPC接口,作为调用平台服务的解决方案。

中间适配层(SDK)

该层提供对服务端RPC接口的封装,用于前端以及其他第三方模块接入系统的入口。主要包含以下几方面:

  • 以Service封装服务端的Handler接口

  • 前端对象封装,提供对Entity对象的扩展

  • Restful 接口,作为其他Html5产品接入的接口

前端表现层

前端表现,不单单是指传统的HTML5页面,还有诸如Mobile,LSH服务等,所有需要通过SDK接入系统的服务,全部都在这一层。

目前前端已经基于Vaadin构建了一整套技术框架,可以快速实现功能,快速部署。

1.2.3. 平台二次开发架构

下面从多个维度,不同实现复杂度来阐述二次开发的架构

..\images\core\framework 1
..\images\core\framework 4

1.2.4. 平台部署架构

lumos可以采用多种方式来部署:

  • 针对小企业,无集群要求,可以尽可能节省硬件资源,全部放在一台服务器。比较推荐的配置为2台服务器,数据库服务器和应用服务器分开。

  • 针对稍微大型企业,有集群要求,则可以借助于前后端分离的方式部署

  • 针对巨无霸企业,有集群要求,业务量巨大,并发数巨大,可以采用更高级的方式部署

以下为各种情况下的部署示意图

..\images\core\deploy 1
..\images\core\deploy 2
..\images\core\deploy 3

1.3. 环境安装

1.3.1. 软件需求

  1. 开发集成环境(Eclipse,IDEA)

  2. Maven3.6

1.3.2. Maven配置

setting 文件配置
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://maven.apache.org/SETTINGS/1.0.0"
    xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
    <pluginGroups>
    </pluginGroups>

    <proxies>
    </proxies>

    <servers>
        <server>
            <id>amaxgs-libs-release</id>
            <username>xxx</username>
            <password>xxx</password> (1)
        </server>
    </servers>

    <mirrors>
        <mirror>
            <id>ags-vaadin-addons</id>
            <name>ags-vaadin-addons</name>
            <mirrorOf>vaadin-addons</mirrorOf>
            <url>http://119.119.118.149:8081/artifactory/remote-vaadin-addons</url>
        </mirror>
    </mirrors>

    <profiles>
        <profile>
            <id>amaxgs-libs</id>
            <repositories>
                <repository>
                    <snapshots>
                        <enabled>false</enabled>
                    </snapshots>
                    <id>amaxgs-libs-release</id>
                    <name>libs-release</name>
                    <url>http://119.119.118.149:8081/artifactory/libs-release</url>
                </repository>
            </repositories>
        </profile>

    </profiles>

    <activeProfiles>
        <activeProfile>amaxgs-libs</activeProfile>
    </activeProfiles>

</settings>
1 用户名密码等信息请联系相关人员获取

1.4. 开始一个项目

项目结构

本文档只是提供一个项目的推荐样例结构,具体的项目结构可根据实际需求自行调整。

样例项目结构如下:

demo-parent:父项目,主要有项目依赖管理、插件管理以及参数定义。
    |---- demo-sdk:主要有客户端对象、Service接口的定义
    |---- demo-web:主要有页面代码
    |---- demo-app:主要有启动类及配置信息
lumos平台统一使用Maven作为项目管理工具。
创建父项目

创建Maven父项目lumos-demo-parent,pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 父项目为lumosframework的父项目,其中包含lumosframework项目模块的依赖管理 -->
    <parent>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-appbase-parent</artifactId>
        <version>3.2.0</version>
    </parent>

    <artifactId>lumos-demo-with-customizedobject-parent</artifactId>
    <packaging>pom</packaging>

    <properties>
    </properties>

    <modules>
        <module>lumos-demo-sdk</module>
        <module>lumos-demo-web</module>
        <module>lumos-demo-app</module>
    </modules>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.ags.lumosframework</groupId>
                <artifactId>lumos-demo-with-customizedobject-sdk</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>com.ags.lumosframework</groupId>
                <artifactId>lumos-demo-with-customizedobject-web</artifactId>
                <version>${project.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                        <encoding>utf-8</encoding>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>
创建SDK项目

创建子项目lumos-demo-sdk,pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-demo-with-customizedobject-parent</artifactId>
        <version>3.2.0</version>
    </parent>

    <artifactId>lumos-demo-with-customizedobject-sdk</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.ags.lumosframework</groupId>
            <artifactId>lumos-appbase-sdk</artifactId>
        </dependency>
    </dependencies>
</project>
创建web项目

创建子项目lumos-demo-web,pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-demo-with-customizedobject-parent</artifactId>
        <version>3.2.0</version>
    </parent>
    <artifactId>lumos-demo-with-customizedobject-web</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.ags.lumosframework</groupId>
            <artifactId>lumos-appbase-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.ags.lumosframework</groupId>
            <artifactId>lumos-demo-with-customizedobject-sdk</artifactId>
        </dependency>
    </dependencies>
</project>
创建最终打包APP项目
单体应用

创建子项目lumos-demo-app,pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-demo-with-customizedobject-parent</artifactId>
        <version>3.2.0</version>
    </parent>
    <artifactId>lumos-demo-with-customizedobject-web</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.ags.lumosframework</groupId>
            <artifactId>lumos-appbase-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.ags.lumosframework</groupId>
            <artifactId>lumos-demo-with-customizedobject-sdk</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <!-- 使用Vaadin作为前端框架需要加入此插件用来编译Widgetset -->
            <plugin>
                <groupId>com.vaadin</groupId>
                <artifactId>vaadin-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>update-widgetset</goal>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

添加启动类DemoApp:

package com.ags.lumosframework;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;

@SpringBootApplication(exclude = HibernateJpaAutoConfiguration.class)
public class DemoApp {

    public static void main(String[] args) {

        SpringApplication.run(DemoApp.class, args);
    }

}

在resources目录下添加配置文件:
可根据部署模式(单机环境还是集群环境)配置相关信息,具体配置请参考安装文档中有关 application.properties 的介绍。

项目结构如下:

customizedobject

选中lumos-demo-parent项目,执行maven的package命令,启动入口DemoApp类。启动完成后, 访问URL:http://localhost:8080/demo 页面如下,初始管理员用户名及密码为admin/P@ssw0rd。 至此,项目搭建完成。

logon
Figure 1. 登录页面
home
Figure 2. 首页

1.5. 实现一个自定义表的增删改查

1.5.1. 定义自定义表对象类

创建一个类,继承 CustomizedObjectSupportBean 对象:

package com.ags.lumosframework.doc.demo.customizedobject.domain;

import java.time.ZonedDateTime;

import com.ags.lumosframework.sdk.base.domain.CustomizedObjectSupport;
import com.ags.lumosframework.sdk.base.entity.IDynamicEntity;

public class Person extends CustomizedObjectSupport {

    public static final String TABLE_NAME = "PERSON";
    public static final String NAME = "NAME";
    public static final String AGE = "AGE";
    public static final String BIRTHDAY = "BIRTHDAY";

    public Person() {
        super(TABLE_NAME);
    }

    public Person(IDynamicEntity dynamicEntity) {
        super(dynamicEntity);
    }

    public String getPersonName() {
        return getAsString(NAME);
    }

    public void setPersonName(String name) {
        set(NAME, name);
    }

    public int getAge() {
        return getAsInt(AGE) == null ? 0 : getAsInt(AGE);
    }

    public void setAge(int age) {
        set(AGE, age);
    }

    public ZonedDateTime getBirthday() {
        return getAsZonedDateTime(BIRTHDAY);
    }

    public void setBirthday(ZonedDateTime birthday) {
        set(BIRTHDAY, birthday);
    }

}

其中NAME,AGE,BIRTHDAY是列名,必须和界面上添加的保持一致。

AddCustomizedTable
自定义表字段名不能是数据库的关键字,否则添加不成功。
1. 创建自定义表平台会在数据库中创建“UT_”开头的表。 2. 自定义表增加了版本控制空能:类似于JPA中的@Version

自定义表字段有八种数据类型可供选择,其中Object类型字段会默认创建对应的外键引用,因此在设值时,需要注意置空:

public void setUser(User user) {
    if (user != null) {
        set(ASSIGN_USER, user.getId());
    } else {
        set(ASSIGN_USER, null);
    }
}

定义表数据类型提供了大文本和二进制类型的支持: 新增对象时需要调用setAsClob和setAsBlob, 修改时可先getAsBlob或getAsClob获取对象后再更新内容(修改时LumosBlob或LumosClob的ID必须带上)

public class CustomizedObjectSupport{
    /**
     * 查询二进制对象
     * 获取二进制流byte[]:LumosBlob.getContent()
     * @param column
     * @return
     */
    public LumosBlob getAsBlob(String column) {
        ILumosBlobService blobService = BeanManager.getService(ILumosBlobService.class);
        return blobService.getById(getAsLong(column));
    }
    /**
     * 查询大文本对象
     * 获取大文本内容String:LumosClob.getContent()
     * @param column
     * @return
     */
    public LumosClob getAsClob(String column) {
        ILumosClobService clobService = BeanManager.getService(ILumosClobService.class);
        return clobService.getById(getAsLong(column));
    }

    /**
     * 新增二进制对象
     * @param column
     * @param value
     */
    public void setAsBlob(String column, LumosBlob value) {
        getColumns().put(column, value);
    }
    /**
     * 新增大文本数据
     * @param column
     * @param value
     */
    public void setAsClob(String column, LumosClob value) {
        getColumns().put(column, value);
    }

}

同时自定义对象基类还提供了大量通用的字段,当然,这些字段都不需要再界面上手动添加字段定义:

public interface IBaseEntity extends ISetDataId, IGetData {

    void setCompanyId(long companyId);

    void setPlatformId(String platformId);

    void setCreateTime(ZonedDateTime createTime);

    void setCreateUserId(long createUserId);

    void setCreateUserName(String createUserName);

    void setCreateUserFullName(String createUserFullName);

    void setCreateIp(String createIp);

    void setDataPermissionPredicate(String dataPermissionPredicate);

    /**
     * 内部使用
     *
     * @return
     */
    void setCreateBid(long creationBid);

    void setLastModifyTime(ZonedDateTime lastModifyTime);

    void setLastModifyUserId(long lastModifyUserId);

    void setLastModifyUserName(String lastModifyUserName);

    void setModifyBid(long modifyBid);

    void setRowLogId(String rowLogId);

    void setLastModifyUserFullName(String lastModifyUserFullName);

    void setLastModifyIp(String lastModifyIp);

    void setDeleteTime(ZonedDateTime deleteTime);

    void setDeleteUserId(long deleteUserId);

    void setDeleteUserName(String deleteUserName);

    void setDeleteIp(String deleteIp);

    void setDeleted(boolean deleted);

    void setDeleteUserFullName(String deleteUserFullName);

}

1.5.2. 定义自定义对象Service类

如果需要对数据库进行访问,需要写一个自定义对象的service接口及service的实现类,接口继承自ICustomizedObjectSupportService,实现类继承自AbstractCustomizedObjectSupportService。

package com.ags.lumosframework.doc.demo.customizedobject.service;

import com.ags.lumosframework.doc.demo.customizedobject.domain.Person;
import com.ags.lumosframework.sdk.base.service.api.ICustomizedObjectSupportService;

public interface IPersonService extends ICustomizedObjectSupportService<Person> {}
package com.ags.lumosframework.doc.demo.customizedobject.service.impl;

import com.ags.lumosframework.sdk.base.service.AbstractCustomizedObjectSupportService;
import org.springframework.stereotype.Service;

import com.ags.lumosframework.doc.demo.customizedobject.domain.Person;
import com.ags.lumosframework.doc.demo.customizedobject.service.IPersonService;

@Service
public class PersonService extends AbstractCustomizedObjectSupportService<Person> implements IPersonService {

    @Override
    public String getTableName() {
        return Person.TABLE_NAME;
    }
}

平台提供的service基类已经提供了很多常用的方法:

/**
 * 用于自定义表二次开发时自定义对象的service接口
 *
 * @param <T>
 */
public interface ICustomizedObjectSupportService<T extends ICustomizedObjectSupport> extends IBaseService {

    /**
     * 添加自定义对象
     *
     * @param obj
     * @return
     */
    T save(T obj);

    /**
     * 添加多个自定义对象
     *
     * @param objs
     */
    void saveAll(List<T> objs);

    /**
     * 根据id删除自定义对象
     *
     * @param objId
     */
    void deleteById(long objId);

    /**
     * 删除一个自定义对象
     *
     * @param obj
     */
    void delete(T obj);

    /**
     * 根据id查询一个自定义对象
     *
     * @param id
     * @return
     */
    T getById(long id);

    /**
     * 根据DynamicEntityFilter条件查询自定义对象
     *
     * @param entityFilter
     * @return
     */
    T getByFilter(DynamicEntityFilter entityFilter);

    /**
     * 查询所有自定义对象
     *
     * @return
     */
    List<T> list();

    /**
     * 根据条件查询自定义对象列表
     *
     * @param entityFilter
     * @return
     */
    List<T> listByFilter(DynamicEntityFilter entityFilter);

    /**
     * 查询数量
     *
     * @param entityFilter
     * @return
     */
    int countByFilter(DynamicEntityFilter entityFilter);

    String getTableName();
}

同时service基类还提供了一个通用的查询方法,使用DynamicEntityFilter指定多个查询条件进行查询。

PS: 如果不想增加实现类(继承AbstractCustomizedObjectSupportService), 直接想调用自定义表的相关接口, 可以用IRawCustomizedObjectSupportService, 对应的类是RawCustomizedObjectSupportService

1.5.3. 添加ui

在demo-web项目中添加UI。

package com.ags.lumosframework.web.ui;

import com.ags.lumosframework.web.vaadin.base.BaseUIHasMenu;
import com.ags.lumosframework.web.vaadin.base.annotation.WebEntry;
import com.vaadin.annotations.Theme;
import com.vaadin.spring.annotation.SpringUI;

@WebEntry(shortCaption = "Demo", longCaption = "示例", description = "This is a demo module", iconPath = "images/mes.png",
    order = 1)
@SpringUI(path = "Demo")
@Theme("light")
public class DemoUI extends BaseUIHasMenu {

    /**
     *
     */
    private static final long serialVersionUID = -924112924167675118L;

    @Override
    protected void setTitle() {

    }
}

指定需要扫描的包或类。

在demo-web项目中添加Spring的Config类:

package com.ags.lumosframework.web.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = {"com.ags.lumosframework.web.ui", "com.ags.lumosframework.web.i18n"})
public class DemoWebAutoConfig {}

在demo-web项目的resources/META-INF目录下创建spring.factories文件:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ags.lumosframework.demo.sdk.config.DemoSDKAutoConfig
每个受Spring管理的Bean的项目都需要在项目中指定需要扫描的包或类。

1.5.4. 添加view

在demo-web项目中添加View。

package com.ags.lumosframework.web.ui.view.demo;

import com.ags.lumosframework.common.exception.PlatformException;
import com.ags.lumosframework.demo.sdk.domain.Book;
import com.ags.lumosframework.demo.sdk.service.IBookService;
import com.ags.lumosframework.web.common.i18.I18NUtility;
import com.ags.lumosframework.web.common.i18.I18Support;
import com.ags.lumosframework.web.common.security.annotation.Secured;
import com.ags.lumosframework.web.constant.DemoPermissionConstants;
import com.ags.lumosframework.web.ui.DemoUI;
import com.ags.lumosframework.web.vaadin.base.BaseView;
import com.ags.lumosframework.web.vaadin.base.ConfirmDialog;
import com.ags.lumosframework.web.vaadin.base.ConfirmResult;
import com.ags.lumosframework.web.vaadin.base.CoreTheme;
import com.ags.lumosframework.web.vaadin.base.annotation.Menu;
import com.ags.lumosframework.web.vaadin.component.paginationobjectlist.IDomainObjectGrid;
import com.ags.lumosframework.web.vaadin.component.paginationobjectlist.PaginationDomainObjectList;
import com.vaadin.icons.VaadinIcons;
import com.vaadin.navigator.ViewChangeListener;
import com.vaadin.spring.annotation.SpringView;
import com.vaadin.ui.Button;
import com.vaadin.ui.Component;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.VerticalLayout;
import org.springframework.beans.factory.annotation.Autowired;

import java.text.NumberFormat;
import java.util.Optional;

@Menu(caption = "Book", captionI18NKey = "demo.book.view.caption", iconPath = "images/icon/text-blob.png", order = 0)
@SpringView(name = "Book", ui = DemoUI.class)
public class BookView extends BaseView implements Button.ClickListener {

    /**
     *
     */
    private static final long serialVersionUID = 8141409383766735767L;

    @Secured(DemoPermissionConstants.BOOK_ADD)
    @I18Support(caption = "Add", captionKey = "common.add")
    private Button btnAdd = new Button();

    @Secured(DemoPermissionConstants.BOOK_EDIT)
    @I18Support(caption = "Edit", captionKey = "common.edit")
    private Button btnEdit = new Button();

    @Secured(DemoPermissionConstants.BOOK_DELETE)
    @I18Support(caption = "Delete", captionKey = "common.delete")
    private Button btnDelete = new Button();

    @Secured(DemoPermissionConstants.BOOK_REFRESH)
    @I18Support(caption = "Refresh", captionKey = "common.refresh")
    private Button btnRefresh = new Button();

    private Button[] btns = new Button[] {btnAdd, btnEdit, btnDelete, btnRefresh};

    private HorizontalLayout hlToolBox = new HorizontalLayout();

    private IDomainObjectGrid<Book> objectGrid = new PaginationDomainObjectList<>();

    @Autowired
    private AddBookDialog addBookDialog;

    @Autowired
    private IBookService bookService;

    public BookView() {
        VerticalLayout vlRoot = new VerticalLayout();
        vlRoot.setMargin(false);
        vlRoot.setSizeFull();

        hlToolBox.setWidth("100%");
        hlToolBox.addStyleName(CoreTheme.TOOLBOX);
        hlToolBox.setMargin(true);
        vlRoot.addComponent(hlToolBox);
        HorizontalLayout hlTempToolBox = new HorizontalLayout();
        hlToolBox.addComponent(hlTempToolBox);
        for (Button btn : btns) {
            hlTempToolBox.addComponent(btn);
            btn.addClickListener(this);
            btn.setDisableOnClick(true);
        }
        btnAdd.setIcon(VaadinIcons.PLUS);
        btnEdit.setIcon(VaadinIcons.EDIT);
        btnDelete.setIcon(VaadinIcons.TRASH);
        btnRefresh.setIcon(VaadinIcons.REFRESH);

        objectGrid.addColumn(Book::getName).setCaption(I18NUtility.getValue("demo.book.name", "Name"));
        objectGrid.addColumn(Book::getIntroduction)
            .setCaption(I18NUtility.getValue("demo.book.introduction", "Introduction"));
        objectGrid.addColumn(source -> {
            double price = source.getPrice();
            return NumberFormat.getInstance().format(price);
        }).setCaption(I18NUtility.getValue("demo.book.price", "Price"));
        objectGrid.setObjectSelectionListener(event -> {
            setButtonStatus(event.getFirstSelectedItem());
        });
        vlRoot.addComponents((Component)objectGrid);
        vlRoot.setExpandRatio((Component)objectGrid, 1);

        this.setSizeFull();
        this.setCompositionRoot(vlRoot);
    }

    private void setButtonStatus(Optional<Book> optional) {
        boolean enable = optional.isPresent();
        btnEdit.setEnabled(enable);
        btnDelete.setEnabled(enable);
    }

    @Override
    protected void init() {
        objectGrid.setServiceClass(IBookService.class);
    }

    @Override
    public void enter(ViewChangeListener.ViewChangeEvent event) {
        setButtonStatus(Optional.empty());
        objectGrid.refresh();
    }

    @Override
    public void buttonClick(Button.ClickEvent event) {
        Button button = event.getButton();
        button.setEnabled(true);
        if (btnAdd.equals(button)) {
            addBookDialog.setObject(null);
            addBookDialog.show(getUI(), result -> {
                if (ConfirmResult.Result.OK.equals(result.getResult())) {
                    objectGrid.refresh();
                }
            });
        } else if (btnEdit.equals(button)) {
            Book book = (Book)objectGrid.getSelectedObject();
            addBookDialog.setObject(book);
            addBookDialog.show(getUI(), result -> {
                if (ConfirmResult.Result.OK.equals(result.getResult())) {
                    Book temp = (Book)result.getObj();
                    objectGrid.refresh(temp);
                }
            });
        } else if (btnDelete.equals(button)) {
            ConfirmDialog.show(getUI(),
                I18NUtility.getValue("common.suretodelete", "Are you sure to delete the selected item?"), result -> {
                    if (ConfirmResult.Result.OK.equals(result.getResult())) {
                        try {
                            bookService.delete((Book)objectGrid.getSelectedObject());
                        } catch (PlatformException e) {
                            notificationError("Common.RelationShipCheckFailed", e.getMessage());
                            return;
                        }
                        objectGrid.refresh();
                    }
                });
        } else if (btnRefresh.equals(button)) {
            objectGrid.refresh();
        }
    }
}

继续添加Dialog类用于添加/修改对象。

package com.ags.lumosframework.web.ui.view.demo;

import org.springframework.context.annotation.Scope;

import com.ags.lumosframework.demo.sdk.domain.Book;
import com.ags.lumosframework.demo.sdk.service.IBookService;
import com.ags.lumosframework.web.common.i18.I18NUtility;
import com.ags.lumosframework.web.common.i18.I18Support;
import com.ags.lumosframework.web.vaadin.base.BaseDialog;
import com.ags.lumosframework.web.vaadin.base.DialogCallBack;
import com.ags.lumosframework.web.vaadin.constants.VaadinCommonConstant;
import com.vaadin.data.Binder;
import com.vaadin.data.converter.StringToDoubleConverter;
import com.vaadin.data.validator.DoubleRangeValidator;
import com.vaadin.spring.annotation.SpringComponent;
import com.vaadin.ui.Component;
import com.vaadin.ui.TextField;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;

@SpringComponent
@Scope("prototype")
public class AddBookDialog extends BaseDialog {

    @I18Support(caption = "Name", captionKey = "demo.book.name")
    private TextField tfName = new TextField();

    @I18Support(caption = "Introduction", captionKey = "demo.book.introduction")
    private TextField tfIntroduction = new TextField();

    @I18Support(caption = "Price", captionKey = "demo.book.price")
    private TextField tfPrice = new TextField();

    private Binder<Book> binder = new Binder<>();

    private String caption;

    private Book book;

    private IBookService bookService;

    public AddBookDialog(IBookService bookService) {
        this.bookService = bookService;
    }

    public void setObject(Book book) {
        String captionName = I18NUtility.getValue("demo.book.caption", "Book");
        if (book == null) {
            this.caption = I18NUtility.getValue("common.new", "New", captionName);
            book = new Book();
        } else {
            this.caption = I18NUtility.getValue("common.modify", "Modify", captionName);
        }
        this.book = book;
        binder.readBean(book);
    }

    @Override
    public void show(UI parentUI, DialogCallBack callBack) {
        setHeightUnDefinedMode();
        showDialog(parentUI, caption, VaadinCommonConstant.MEDIUM_DIALOG_WIDTH, null, false, true, callBack);
    }

    @Override
    protected void initUIData() {
        binder.forField(tfName)
            .asRequired(I18NUtility.getValue("common.requiredfilednotempty", "Required filed cannot be empty"))
            .bind(Book::getName, Book::setName);
        binder.bind(tfIntroduction, Book::getIntroduction, Book::setIntroduction);
        binder.forField(tfPrice)
            .withConverter(
                new StringToDoubleConverter(I18NUtility.getValue("Common.OnlyFloatAllowed", "Only float is allowed")))
            .withValidator(new DoubleRangeValidator(
                I18NUtility.getValue("Common.MustEqualOrLargerThan0", "Must equals or larger than 0"), 0D,
                Double.MAX_VALUE))
            .bind(Book::getPrice, Book::setPrice);
    }

    @Override
    protected void okButtonClicked() throws Exception {
        binder.writeBean(book);
        Book save = bookService.save(book);
        result.setObj(save);
    }

    @Override
    protected void cancelButtonClicked() {}

    @Override
    protected Component getDialogContent() {
        VerticalLayout vlContent = new VerticalLayout();
        vlContent.setSizeFull();

        tfName.setWidth("100%");
        tfIntroduction.setWidth("100%");
        tfPrice.setWidth("100%");

        vlContent.addComponents(tfName, tfIntroduction, tfPrice);
        return vlContent;
    }

}

1.5.5. 添加权限

  • 在demo-web项目中定义权限常量:

package com.ags.lumosframework.web.constant;

public class DemoPermissionConstants {

    public static final String BOOK_ADD = "demo.book.add";
    public static final String BOOK_EDIT = "demo.book.edit";
    public static final String BOOK_DELETE = "demo.book.delete";
    public static final String BOOK_REFRESH = "demo.book.refresh";

}
  • 在页面组件上加 @Secured 注解,参考View类中的写法。

    需要注意的是:`@Secured` 注解所在的页面必须是spring的bean
@Secured(DemoPermissionConstants.BOOK_ADD)//添加权限
@I18Support(caption = "Add", captionKey = "common.add")//添加国际化
private Button btnAdd = new Button();
  • 根据 @Secured 注解, 动态生成权限

当加上以下配置, 启动项目的时候, 系统自动根据@Secured的配置生成对应的权限(Permission数据),默认为false

lumos.permission.auto.create.enable=true

包含 @Secured 注解的类必须是spring的bean;

通过"SecurityUtils"来判断权限的, 必须手动添加创建permission的脚本;

  • 在页面中也能创建权限,其中名称要与DemoPermissionConstants中常量值保持一致。 模块和类别可以在原有的数据里选择, 也可以新加, 新加时对应的国际化必须是国际化文件里有的

CustomizedTable Permission

1.5.6. 添加国际化

平台提供了统一的国际化支持,详情请查阅国际化支持
1、首先,添加国际化文件 Demo_I18_Normal_zh_CN.properties 及 Demo_I18_Normal_en_US.properties,内容为国际化 Key 及 Value 的键值对。
并将这些文件放置在demo-web项目的指定路径下,路径为:src/main/resources/locale/。 properties 文件的代码示例如下:

demo.book.view.caption=书籍
demo.book.caption=书籍
demo.book.name=书名
demo.book.introduction=简介
demo.book.price=价格

2、在demo-web项目中添加DemoI18NProvider类,用于指定国际化文件路径。

package com.ags.lumosframework.web.i18n;

import org.springframework.stereotype.Component;

import com.ags.lumosframework.web.common.i18.I18NProvider;

@Component
public class DemoI18NProvider implements I18NProvider {

    @Override
    public String[] getBaseNames() {
        return new String[] {"locale/Demo_I18_Normal"};
    }

    @Override
    public int getPriority() {
        return 0;
    }

}

1.6. 首页及登录页

可以在系统配置-主页Logo设置里配置主页Logo

homelogo
Figure 3. 配置主页LOGO

1.6.2. 首页及登录页的定制化

首页及登录页是html类型的文件,您可以编写自己的登录页和首页,并在项目的配置文件里指向此地址。
具体方法如下:
1、编写自己的login.html/index.html,并将其放在前端项目的 src/java/resources/static 的某文件夹中。目录结构如下图所示:

path login
Figure 4. 项目下html文件所在目录

2、在 application.properties 中添加如下配置项:

lumos.login-url=/mes/login.html
lumos.index-url=mes/index.html

另外,平台的login.html和index.html可以在lumos-appbase-web的jar中找到:位于\static\platform目录下。

1.7. 国际化支持

Lumos本身提供了多种国际化方式,统一的接口,可以帮助使用者简化大量操作。

1.7.1. 利用国际化接口及实现

以下接口为平台提供,可以由其他模块实现的接口,如下:

public interface I18NProvider {

    /**
     * 返回该模块的国际化文件的名称。 该国际化文件一般放在 maven 项目的{reource_folder}下面
     *
     * @return
     */
    String[] getBaseNames();

    /**
     * 返回该模块的国际化文件的优先级,低优先级的会被高优先级的覆盖掉。
     *
     * @return
     */
    int getPriority();

}
国际化文件声明

如果需要在新项目上增加本地化支持,需要自定义一个DemoProvider来实现接口I18Nprovider,并且实现里面的方法,getBaseNames方法是设置本地化文件的命名规则前缀。getPriority方法是设置他的优先级。

如你可以定义如下国际化文件信息

@Component
public class DemoI18NProvider implements I18NProvider {
   @Override
   public String[] getBaseNames() {
      return new String[] {"locale/Demo_I18_Normal", "locale/Demo_I18_Error"};
   }
   @Override
   public int getPriority() {
      return 1;
   }
}
  • locale/Demo_I18_Normal 和为国际化文件的地址,可以指定多个。

确保该类定义在SpringBoot的扫描路径中,否则不生效。
国际化文件创建

在如下位置加入国际化文件,国际化文件统一为properties文件。

..\images\core\i18n

1.7.2. 使用系统对象支持国际化

除了使用国际化文件支持国际化的定义之外,Lumos也提供了I18NText对象,用户可以使用该对象对系统的国际化文本进行扩展。

..\images\core\i18neditor

如上为I18N国际化文本的定义以及在不同语言中显示的值,通过该方法的好处是用户可以在页面中直接修改,使用他们更加喜欢的值,此方式更加灵活。
在二次开发中,这个是比较推荐的一种方式。

如上样例中,我们定义一个TextId,名称为Report.Message,那么在Report页面中,当用户添加Report的时候,就可以为定制化报表自定义菜单要显示的内容。

..\images\core\i18n report

1.7.3. 系统使用国际化

vaadin控件使用国际化

vaadin的控件可以直接通过Annotation的形式直接使用国际化,目前支持注入Button,Label,TextField,ComboBox等控件。使用方式直接在类型声明上面使用如下注解即可,如:

@I18Support(caption = "Add", captionKey = "common.add")
private Button btnAdd = new Button("Add");
系统信息使用国际化

如果是在处理逻辑中需要使用国际化信息,那么可以通过以下接口拿到国际化:

//主要是通过该方法拿到国际化信息
I18NUtility.getValue("Demo.Person.IdNo", "Id Card NO")

//如以下场景:
gridPersons.addColumn(Person::getIdNo).setCaption(I18NUtility.getValue("Demo.Person.IdNo", "Id Card NO"));
gridPersons.addColumn(Person::getName).setCaption(I18NUtility.getValue("Demo.Person.Name", "Name"));
gridPersons.addColumn(Person::getAge).setCaption(I18NUtility.getValue("Demo.Person.Age", "Age"));
gridPersons.addColumn(Person::getBirthday).setCaption(I18NUtility.getValue("Demo.Person.Birthday", "Birthday"));

1.8. 扩展属性

扩展属性的作用就是在原对象的基础上进行扩展,原对象缺少字段,不能满足项目的需求,这个时候可以使用扩展属性。

只有支持扩展属性的对象才能添加扩展属性,如果想让目前不支持的对象也能添加扩展属性, 则需要保证此类实现了 IBaseEntityExtension 接口,并创建一个类实现接口 CustomizedFieldObjectTypeProvider ,将需要支持扩展属性的对象类型返回即可:

public interface CustomizedFieldObjectTypeProvider {
    /**
     *
     * @return 支持拓展属性的类集合
     */
    List<CustomizedFileldObjectSupport> getObjectTypes();

}

平台提供了User,UserGroup,JobPosition,Role,Permission,Department,Doc等对象的扩展属性支持。

@Component
public class LumosCustomizedFieldObjectTypeProvider implements CustomizedFieldObjectTypeProvider {

    public static CustomizedFileldObjectSupport[] cfSupportList =
        {new CustomizedFileldObjectSupport(CoreserviceObjectType.User, "admin.user"),
            new CustomizedFileldObjectSupport(CoreserviceObjectType.UserGroup, "admin.usergroup"),
            new CustomizedFileldObjectSupport(CoreserviceObjectType.Role, "admin.role"),
            new CustomizedFileldObjectSupport(CoreserviceObjectType.Permission, "admin.permission"),
            new CustomizedFileldObjectSupport(CoreserviceObjectType.JobPosition, "admin.jobposition"),
            new CustomizedFileldObjectSupport(CoreserviceObjectType.Doc, "admin.doc"),
            new CustomizedFileldObjectSupport(CoreserviceObjectType.Department, "admin.organization")};

    @Override
    public List<CustomizedFileldObjectSupport> getObjectTypes() {
        return Arrays.asList(cfSupportList);
    }

}

1.8.1. 系统对象扩展属性添加

在扩展属性页面添加所需对象的扩展属性,平台提供了String,Boolean,Integer,Long,Float,BigDecimal,Time,Object八种类型,其中Time类型,对应的java类型为ZonedDateTime,Object类型为一个Long类型的字段,保存对应Object的Id字段值,使用Object类型字段时系统会默认创建一个对应的外键引用。

AddCustomizedField
扩展属性名不能是数据库的关键字,否则添加不成功。
创建扩展属性会在数据库中创建“CF_”开头的扩展属性表。e.g. 创建Part的扩展属性,则平台会在数据库中创建CF_PART表用于保存Part的扩展属性值。

1.8.2. 设值及持久化

为方便在代码中使用新增的字段,通常在代码中添加字段常量,类名的命名规则建议为对象名加上CEF ,例如:

public class UserCEF {
    public static final String USER_CF_USED_NAME = "USED_NAME";
    public static final String USER_CF_AGE = "AGE";
    public static final String USER_CF_BIRTHDAY = "BIRTHDAY";
}

设值使用setCEF()方法,调用对应service的save()方法保存:

user.setCEF(UserCEF.USER_CF_USED_NAME, "张三");
userService.save(user);

1.8.3. 扩展系统对象

对象扩展属性的功能有两种实现方式:
第一种是上面讲的直接使用系统对象,然后调用setCEF()等方法,但如果扩展属性过多,使用起来可能不太方便,系统还提供了第二种方式,即直接扩展系统对象,加上相应的get/set方法,方便调用。

使用方法如下(以扩展User对象为例):

首先,扩展User对象
public class ExtensionUser extends User {

    public static final String EXTENSION_PARAM_BIRTHDAY = "birthday";

    public String getBirthday(){
        return (String) getCEF(EXTENSION_PARAM_BIRTHDAY);
    }

    public void setBirthday(String birthday){
        setCEF(EXTENSION_PARAM_BIRTHDAY, birthday);
    }

}
然后,扩展User的服务
  • 定义接口继承自需要扩展service的接口 以UserService为例:添加IExtensionUserService接口继承自IUserService接口:

public interface IExtensionUserService extends IUserService {

    void extensionBusiness();

}
  • 定义扩展类继承自需要的Service类

继续以Uservice为例,添加ExtensionUserService类继承UserService实现IExtensionUserService接口:

@Service
@Primary
public class ExtensionUserService extends UserService implements IExtensionUserService {
    @Override
    public void extensionBusiness() {
    // do something
    }
}
@Primary注解表示有多个实例时,默认使用哪一个,所以必须在扩展服务上加上此注解,否则程序会报错。

1.9. 服务发布与消费

项目在不做额外配置下是不会对外发布服务的, 本章节描述如何发布服务,以及其他模块如何消费服务

1.9.1. 服务的发布

  • 平台服务发布功能是默认开启,如果关闭, 可在配置文件里面配置

    lumos.rpc.enable=false
  • 平台sdk项目所提供的handler接口默认都添加了@RpcSupport注解, 该注解标记该接口可以用于服务的发布和消费, 其他项目如果提供了handler接口也可以使用该注解标记, 以下是平台的IUserHandler

    @RpcSupport
    public interface IUserHandler extends IBaseEntityHandler<UserEntity> {
    
    }
  • 对于在sdk项目中使用了@RpcSupport注解的,都需要在任意一个有效的配置类上使用@RpcServiceScan注解,以配置扫描到该注解, @RpcServiceScan会扫描包下面所有配置了@RpcSupport注解的接口, 若容器中有该接口的实现类,则发布该服务到注册中心, 若没有,则创建该接口的消费代理, 以下是平台某个sdk中的配置类

    @Configuration
    @RpcServiceScan({"com.ags.lumosframework.sdk.handler.api", "com.ags.lumosframework.sdk.base.handler.api"})
    public class LumosCommonSDKAutoConfiguration {
    
    }
  • 在app项目的pom文件中,添加zookeeper相关依赖

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
            </dependency>
  • 在app项目的application.properties配置文件添加下面配置信息

    #配置zookeeper的连接信息
    spring.cloud.zookeeper.connect-string=localhost:2181 (1)
    dubbo.protocol.name=dubbo
    dubbo.protocol.port=20991 (2)
    1 zookeeper的连接信息
    2 服务对外发布的端口

1.9.2. 服务的消费

  • 在app项目的pom文件中,添加zookeeper相关依赖

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
            </dependency>
  • 在app项目的application.properties配置文件添加下面配置信息

    #配置zookeeper的连接信息
    spring.cloud.zookeeper.connect-string=localhost:2181 (1)
    dubbo.protocol.name=dubbo
    dubbo.protocol.port=20991 (2)
    1 zookeeper的连接信息
    2 服务对外发布的端口
  • 在消费服务时,会将上下文的用户信息(RequestInfo)传递到服务提供者端,目前平台已经堆所有页面请求做了处理, 会自动带上实际的页面用户信息,如果请求不是来自页面,比如来自自己写的后台任务,则可能没有用户信息,针对这种情况, 平台提供了 rpc.default.user-id rpc.default.user-name 两个配置属性,配置在application.properties中,用于表示 默认的用户id和默认的用户名

1.9.3. 公共模块多个库下如何调用

场景:mes和 wms在使用同一个zookeeper
问题:mes和wms都使用了appbase模块,都有用户,权限,自定义表等公共的api, 如何确保在lsh中准确调用mes或者wms的公共api

平台的公共代码默认提供了用户,角色,权限等业务逻辑,当一个项目足够大,需要按模块部署时,需要每个模块一个库, 这种情况下公共代码中提供的接口在多个库下会有多个实现,如果所有模块都向服务注册中心注册,则会出现对接口的调用随机访问各个模块实现的问题, 这种开发情况下需要添加额外的配置才可以解决模块服务之间调用公共接口混乱的问题

服务提供者
  • 服务提供者不需要做任何的配置

服务消费者
  • 在消费端,对于公共的模块接口,可以通过配置指定默认调用的应用名称(即发布者服务对应的spring.application.name)

    lumos.rpc.packages[com.ags.lumosframework.sdk.handler.api]=wms (1)
    lumos.rpc.packages[com.ags.mes.server.api.handler]=mes
    1 该配置则表示 com.ags.lumosframework.sdk.handler.api 包下面的所有接口都会调用 wms
  • 消费端也提供的声明式的方式声明接口走的是哪个应用,该配置优先级高于上面定义的配置

    @Service
    public class XXXService{
    
        @Autowired
        @To("lumos-basic-app")
        private IUserService userService1; (1)
    
        @Autowired
        @To("lumos-basic-app2")
        private IUserService userService2;
    }
    1 @To("lumos-basic-app") 注解用于声明,所有对该属性的调用都是走的应用 lumos-basic-app

1.10. 事务支持

1.10.1. 本地事务

本地事务表示同一个jvm内,代码调用链出现了调用如数据库等事务资源的代码,需要添加 spring的 @Transactional 注解支持本地事务

本地事务默认由spring直接支持,不需要额外的配置

本地事务一般在handler和dao添加

1.10.2. 分布式事务

在使用rpc调用服务的情况下,需要使用分布式事务,用于将各个调用服务的本地事务变更为一整个分布式事务, 在需要使用的方法上添加 @TxTransaction 注解

分布式事务除了添加注解,还需要项目做些额外的配置才能支持,参照项目如何开启分布式事务支持

正常本地事务都需要支持分布式事务,所以方法上一般会同时存在 @Transactional @TxTransaction 两个注解, 如果方法只存在rpc调用,不存在本地事务资源调用,则只需要添加 @TxTransaction, 例如在使用自定义表做二次开发时,一般只写到service层,而service层不会出现 调用本地资源(数据库等)的代码,所以在service层可以只添加 @TxTransaction

项目如何开启分布式事务支持
  • 从公司下载相关版本的txmanager并启动,具体txmanager的安装使用可以参考安装部署文档中关于txmanager的说明部分

  • 在需要使用@TxTransaction注解的项目(如impl,sdk项目)的pom文件里添加如下依赖

<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-tc</artifactId>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-txmsg-netty</artifactId>
    <scope>provided</scope>
</dependency>
  • 在app启动项目的pom文件里添加如下依赖

<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-tc</artifactId>
</dependency>
<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-txmsg-netty</artifactId>
</dependency>
  • 在app启动项目的application.properties配置文件添加如下信息

# tx-manager 的配置地址,可以指定TM集群中的任何一个或多个地址
# tx-manager 下集群策略,每个TC都会从始至终<断线重连>与TM集群保持集群大小个连接。
# TM方,每有TM进入集群,会找到所有TC并通知其与新TM建立连接。
# TC方,启动时按配置与集群建立连接,成功后,会再与集群协商,查询集群大小并保持与所有TM的连接
tx-lcn.client.manager-address=127.0.0.1:8070
  • 在app启动项目的启动类上添加 @EnableDistributedTransaction(enableTxc = false)

1.11. 基于token的访问控制

平台所有请求都有基于token的验证,没有token的请求无法访问受保护的资源

1.11.1. token获取

rest login

上图的rest接口用于登录获取token , 该接口返回的accessToken的值,在后续的访问请求header中需要加上, header的key为LUMOS-TOKEN, value为返回的accessToken.

对于wen浏览器的请求,登陆后平台已经自动加上相关token,不需要做额外设置, 对于其他客户端,则需要设置

1.11.2. token仓库

平台默认使用数据库存储token, 当有多个模块时,需要切换到redis存储token, 配置如下

lumos.security.token.repository=redis

如何配置redis请参考安装部署文档

1.12. Sequence

lumos 提供了序列号功能,用于支持自增流水号生成规则的定制

1.12.1. 添加一个序列号定义

在管理项页面添加一个序列号定义,选择一个序列号类型

SequenceAdd

1.12.2. 添加一个序列号参数

选中添加的序列号,添加序列号对应的参数

SequenceParamAdd
类型

参数类型,一般在生成序列号时需要传入该参数的key

重置

用于表示在生成序列号时,该参数的改变是否会重置序列号初始值,比如年月日参数重置后,每日都会从0开始

排序序号

表示参数的排序序号,生成序列号时会按照序号顺序拼装参数

进制转换

用于对参数进制进制的装换, 原始进制表示参数值的原始进制数,目标进制表示期望转换的进制数

两端补齐

用于对参数值在长度不够的情况下补齐,补齐位置表示是左边补齐还是右边补齐,长度表示参数值最终的总长度,占位符表示用什么字符去补齐

两端截取

用于对参数值进行截取位数,截取位置表示左边截取还是右边截取, 截取后长度表示参数截取后剩余的长度

参数创建后,编辑时不可以更改参数类型和重置字段

1.12.3. 生成序列号

ISequenceDefService接口提供了接口用于生成对应的序列号,注入该类直接使用

ISequenceDefService接口定义
public interface ISequenceDefService extends IBaseDomainObjectService<SequenceDef> {

    /**
     * @param sequenceDef 上文添加的序列号定义
     * @param params      参数值,可以只包含参数定义中勾选了重置的参数,因为只查询,不生成
     * @return 返回当前的值
     */
    long current(SequenceDef sequenceDef, Map<String, Object> params);

    /**
     * 根据sequence name 查询sequence
     *
     * @param sequenceName sequence name
     * @return 返回sequence定义
     */
    SequenceDef getByName(String sequenceName);

    /**
     * 获取所有有效的序列号定义
     *
     * @return
     */
    List<SequenceDef> listSequenceDefs();

    /**
     * 根据序列号类型获取有效的序列号定义集合
     *
     * @param sequenceType 序列号定义类型
     * @return
     */
    List<SequenceDef> listSequenceDefsBySequenceType(String sequenceType);

    /**
     * @param sequenceDef 上文添加的序列号定义
     * @param params      参数值,与上文添加的参数定义对应,部分参数字段可以不需要传入(年月日,序列号)
     * @return 返回生成的序列号
     */
    String next(SequenceDef sequenceDef, Map<String, Object> params);

    /**
     * @param sequenceDef 上文添加的序列号定义
     * @param params      参数值,与上文添加的参数定义对应,部分参数字段可以不需要传入(年月日,序列号)
     * @param count       生成序列号的数目
     * @return 返回生成的序列号列表
     */
    List<String> next(SequenceDef sequenceDef, Map<String, Object> params, int count);


}

1.12.4. 内置的序列号绑定组件

平台前端提供了基础类,用于简化对象和sequence定义的绑定

  • 实现一个IAssignSequence接口, 该接口一般使用一个关联表来实现

IAssignSequence接口定义
public interface IAssignSequence<T> {

    /**
     * 查询绑定对象的序列号定义
     *
     * @param t
     *            绑定的对象
     * @return
     */
    List<SequenceDef> listAssignedSequenceDefs(T t);

    /**
     * 给对象绑定序列号定义id列表
     *
     * @param t
     * @param sequenceDefIds
     *            序列号定义id列表
     */
    void assignSequenceDefs(T t, Set<Long> sequenceDefIds);

    /**
     * 获取所有的 有效序列号定义
     *
     * @return 有效序列号定义集合
     */
    List<SequenceDef> listSequenceDefs();

    void deleteSequenceRByObjectAndSequenceDefId(T t, long sequenceDefId);

}
  • 在需要绑定sequence对象的页面添加SequenceDefTab, 将SequenceDefTab添加到任意页面即可, sequenceDefTab作为一个组件,可以在相关页面上直接使用,其有一个方法setObject, 用于传递支持绑定sequence的对象

SequenceDefTab sequenceDefTab = new SequenceDefTab<>(assignSequence); (1)
1 构造函数的参数为IAssignSequence接口的实现

1.12.5. 扩展

Sequence类型除了内置的实现, 平台也提供了扩展

Sequence类型扩展

添加扩展的sequence类型,在sequence页面新增sequence时可以选择扩展的类型

ISequenceType接口定义
public interface ISequenceType extends IHasI18Name, IHasName {

    /**
     * sequence名字
     *
     * @return
     */
    @Override
    String getName();

    List<IParameterDef> getParameterDefs();

    /**
     * 是否支持扩展属性
     *
     * @return
     */
    boolean isSupportCustomizedParam();

    /**
     * 默认提供的参数列表
     *
     * @return
     */
    default List<IParameterDef> getDefaultParameterDefs() {
        List<IParameterDef> parameterDefList = new ArrayList<>();
        parameterDefList.add(CommonParameterDefs.YEAR);
        parameterDefList.add(CommonParameterDefs.MONTH);
        parameterDefList.add(CommonParameterDefs.DAY);
        parameterDefList.add(CommonParameterDefs.SERIAL_NUMBER);
        parameterDefList.add(CommonParameterDefs.FIXED_VALUE);
        parameterDefList.add(CommonParameterDefs.SQL);
        return parameterDefList;
    }
}

参数定义参考Sequence参数扩展

Sequence参数扩展

用户可以添加一些参数定义,参数定义包含了参数的名字和参数的类型等信息, 该参数定义可以用于序列号类型表示序列号支持哪些参数, 也可用于格式化器表示格式化器所需的参数。

在添加sequence参数时,列表信息来源于sequence类型支持的参数信息

在对参数信息添加格式化器时,每个格式化器相关的控件配置信息来源于格式化器支持的参数信息

IParameterDef接口定义
public interface IParameterDef extends IParameterVerifiable {

    /**
     * parameter 的key
     *
     * @return
     */
    String getKey();

    /**
     * parameter 的name
     *
     * @return
     */
    String getName();

    /**
     * name 国际化key
     *
     * @return
     */
    String getNameI18NKey();

    ParameterType getType();

    default IParameterValue[] getParameterValues() {
        return null;
    };

    default IParameterValue getDefaultParameterValue() {
        return null;
    }

    @Override
    default void verify(Object value) {};
}
Sequence格式化器扩展

用户可以定义自己的格式化器用于支持对相关的参数进行转换。

lumos 内置提供了NumberRadixFormatter和LeftRightPadFormatter两种格式化器,用于对sequence的参数进行格式化转换

NumberRadixFormatter用于对数值类型的参数进行进制的转换

LeftRightPadFormatter用于对参数进行左边或右边长度填充,例如对1使用字符‘0’进行左边长度为8的填充(000000001)

IFormatter接口定义
public interface IFormatter {

    /**
     * 对原始文本进行格式化
     *
     * @param text
     *            原始文本
     * @param formatterConfig
     *            格式化所需的参数
     * @return
     */
    String format(String text, FormatterConfig formatterConfig);

    /**
     * formatter 的id
     *
     * @return
     */
    String getId();

    /**
     * formatter 的name
     *
     * @return
     */
    String getName();

    /**
     * formatter 的name i18n key
     *
     * @return
     */
    String getNameI18NKey();

    IParameterDef[] getParameterDefs();
}

参数定义参考Sequence参数扩展

实现一个ISequenceType
  • 内置的CommonSequenceType实现

public class CommonSequenceType implements ISequenceType {

    @Override
    public String getName() {
        return "Common";
    }

    @Override
    public String getI18Name() {
        return "common";
    }

    @Override
    public List<IParameterDef> getParameterDefs() {
        List<IParameterDef> parameterDefList = new ArrayList<>(getDefaultParameterDefs());
        return parameterDefList;
    }

    @Override
    public boolean isSupportCustomizedParam() {
        return false;
    }
}
  • 在META-INF/spring.factories添加如下语句

spring.factories
com.ags.lumosframework.sdk.sequence.ISequenceType=com.ags.lumosframework.sdk.sequence.CommonSequenceType
实现一个IParameterDef
  • 内置CommonParameterDefs代码实现

public enum CommonParameterDefs implements IParameterDef {
    /**
     * SQL 根据SQL语句从数据库查询,返回时必须有且仅有一个值
     */
    SQL("SQL", "SQL", "parameter.def.sql", ParameterType.String),
    /**
     * 固定值
     */
    FIXED_VALUE("FIXED_VALUE", "FIXED_VALUE", "parameter.def.fixed-value", ParameterType.String),
    /**
     * 自定义值
     */
    CUSTOMIZED_VALUE("CUSTOMIZED_VALUE", "CUSTOMIZED_VALUE", "parameter.def.customized-value", ParameterType.String),

    /**
     * 流水号(一般表示自增的数值类型)
     */
    SERIAL_NUMBER("SERIAL_NUMBER", "SERIAL_NUMBER", "parameter.def.serial-number", ParameterType.Number),
    /**
     * 日
     */
    DAY("DAY", "DAY", "parameter.def.day", ParameterType.Number),
    /**
     * 月
     */
    MONTH("MONTH", "MONTH", "parameter.def.month", ParameterType.Number),
    /**
     * 年
     */
    YEAR("YEAR", "YEAR", "parameter.def.year", ParameterType.String),

    /**
     * 原始进制,(格式化器使用的参数)
     */
    ORIGIN_RADIX("ORIGIN_RADIX", "ORIGIN_RADIX", "parameter.def.origin-radix", ParameterType.ComplexSingleSelect,
        new IParameterValue[] {NumberRadixValues.RADIX10, NumberRadixValues.RADIX16, NumberRadixValues.RADIX34,
            NumberRadixValues.RADIX36},
        NumberRadixValues.RADIX10),

    /**
     * 目标进制,(格式化器使用的参数)
     */
    TARGET_RADIX("TARGET_RADIX", "TARGET_RADIX", "parameter.def.target-radix", ParameterType.ComplexSingleSelect,
        new IParameterValue[] {NumberRadixValues.RADIX10, NumberRadixValues.RADIX16, NumberRadixValues.RADIX34,
            NumberRadixValues.RADIX36}),
    /**
     * 左补齐还是又补齐,(格式化器使用的参数)
     */
    PAD_POSITION("PAD_POSITION", "PAD_POSITION", "parameter.def.pad-position", ParameterType.ComplexSingleSelect,
        new IParameterValue[] {PadPositionValues.LEFT, PadPositionValues.RIGHT}, PadPositionValues.LEFT),
    /**
     * 补齐长度,(格式化器使用的参数)
     */
    PAD_LENGTH("PAD_LENGTH", "PAD_LENGTH", "parameter.def.pad-length", ParameterType.Number, (value) -> {
        Pattern pattern = Pattern.compile("[0-9]*");
        if (!pattern.matcher(value.toString()).matches()) {
            throw new PlatformException("admin.Sequence.error.notInteger",
                "The sequence rule error: the length must be a positive integer!");
        }
    }),
    /**
     * 补齐的占位符,(格式化器使用的参数)
     */
    PAD_PLACE_HOLDER("PAD_PLACE_HOLDER", "PAD_PLACE_HOLDER", "parameter.def.pad-place-holder", ParameterType.Char),
    /**
     * 左截取还是右截取,(格式化器使用的参数)
     */
    SUB_POSITION("SUB_POSITION", "SUB_POSITION", "parameter.def.sub-position", ParameterType.ComplexSingleSelect,
        new IParameterValue[] {SubPositionValues.LEFT, SubPositionValues.RIGHT}, SubPositionValues.LEFT),
    /**
     * 截取长度,(格式化器使用的参数)
     */
    SUB_LENGTH("SUB_LENGTH", "SUB_LENGTH", "parameter.def.sub-length", ParameterType.Number, (value) -> {
        Pattern pattern = Pattern.compile("[0-9]*");
        if (!pattern.matcher(value.toString()).matches()) {
            throw new PlatformException("admin.Sequence.error.notInteger",
                "the pad or intercept length must be a positive integer!");
        }
    });

    String key;
    String name;
    String nameI18NKey;
    ParameterType parameterType;
    IParameterValue[] parameterValues;
    IParameterValue defaultParameterValue;
    IParameterVerifiable defaultValidator;

    CommonParameterDefs(String key, String name, String nameI18NKey, ParameterType parameterType) {
        this.key = key;
        this.name = name;
        this.nameI18NKey = nameI18NKey;
        this.parameterType = parameterType;
    }

    CommonParameterDefs(String key, String name, String nameI18NKey, ParameterType parameterType,
        IParameterValue[] parameterValues) {
        this(key, name, nameI18NKey, parameterType);
        this.parameterValues = parameterValues;
    }

    CommonParameterDefs(String key, String name, String nameI18NKey, ParameterType parameterType,
        IParameterValue[] parameterValues, IParameterValue defaultParameterValue) {
        this(key, name, nameI18NKey, parameterType);
        this.parameterValues = parameterValues;
        this.defaultParameterValue = defaultParameterValue;
    }

    CommonParameterDefs(String key, String name, String nameI18NKey, ParameterType parameterType,
        IParameterVerifiable defaultValidator) {
        this(key, name, nameI18NKey, parameterType);
        this.defaultValidator = defaultValidator;
    }

    @Override
    public String getKey() {
        return key;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getNameI18NKey() {
        return nameI18NKey;
    }

    @Override
    public ParameterType getType() {
        return parameterType;
    }

    @Override
    public IParameterValue[] getParameterValues() {
        return parameterValues;
    }

    @Override
    public IParameterValue getDefaultParameterValue() {
        return defaultParameterValue;
    }

    @Override
    public void verify(Object value) {
        if (defaultValidator != null) {
            defaultValidator.verify(value);
        }
    }
}
  • 在META-INF/spring.factories添加如下语句

spring.factories
com.ags.lumosframework.common.parameter.IParameterDef=com.ags.lumosframework.common.parameter.CommonParameterDefs
实现一个IFormatter
  • 内置LeftRightPadFormatter实现

public class LeftRightPadFormatter extends AbstractFormatter {

    public static String ID = "LEFT_RIGHT_PAD_FORMATTER";

    public static void main(String[] args) {

        Map<String, Object> stringObjectMap = new HashMap<>();
        stringObjectMap.put(CommonParameterDefs.PAD_POSITION.getKey(), PadPositionValues.RIGHT.getValue());
        stringObjectMap.put(CommonParameterDefs.PAD_PLACE_HOLDER.getKey(), "A");
        stringObjectMap.put(CommonParameterDefs.PAD_LENGTH.getKey(), 10);
        FormatterConfig formatterConfig = new FormatterConfig();
        formatterConfig.addConfigParams(stringObjectMap);

        IFormatter numberRadixFormatter = Formatters.getById(LeftRightPadFormatter.ID);
        String format = numberRadixFormatter.format("123578", formatterConfig);
        System.err.println(format);
    }

    @Override
    public String getId() {
        return ID;
    }

    @Override
    public String getName() {
        return "LeftRightPadFormatter";
    }

    @Override
    public String getNameI18NKey() {
        return "formatters.left-right-pad-formatter";
    }

    @Override
    public IParameterDef[] getParameterDefs() {
        return new IParameterDef[] {CommonParameterDefs.PAD_POSITION, CommonParameterDefs.PAD_LENGTH,
            CommonParameterDefs.PAD_PLACE_HOLDER};
    }

    @Override
    protected String processFormat(String text, FormatterConfig formatterConfig) {
        String padPosition = (String)formatterConfig.getConfigParams().get(CommonParameterDefs.PAD_POSITION.getKey());
        String padPlaceHolder =
            (String)formatterConfig.getConfigParams().get(CommonParameterDefs.PAD_PLACE_HOLDER.getKey());
        Number padLength = (Number)formatterConfig.getConfigParams().get(CommonParameterDefs.PAD_LENGTH.getKey());
        if (PadPositionValues.LEFT.getValue().equals(padPosition)) {
            return StringUtils.leftPad(text, padLength.intValue(), padPlaceHolder);
        } else {
            return StringUtils.rightPad(text, padLength.intValue(), padPlaceHolder);
        }
    }
}
  • 内置NumberRadixFormatter 实现

public class NumberRadixFormatter extends AbstractFormatter {

    public static String ID = "NUMBER_RADIX_FORMATTER";

    public static void main(String[] args) {
        Map<String, Object> stringObjectMap = new HashMap<>();
        stringObjectMap.put(CommonParameterDefs.TARGET_RADIX.getKey(), 36);
        FormatterConfig formatterConfig = new FormatterConfig();
        formatterConfig.addConfigParams(stringObjectMap);

        IFormatter numberRadixFormatter = Formatters.getById(NumberRadixFormatter.ID);
        String format = numberRadixFormatter.format("123578", formatterConfig);
        System.err.println(format);

    }

    @Override
    public String getId() {
        return ID;
    }

    @Override
    public String getName() {
        return "NumberRadixFormatter";
    }

    @Override
    public String getNameI18NKey() {
        return "formatters.number-radix-formatter";
    }

    @Override
    public IParameterDef[] getParameterDefs() {
        return new IParameterDef[] {CommonParameterDefs.ORIGIN_RADIX, CommonParameterDefs.TARGET_RADIX};
    }

    @Override
    protected String processFormat(String text, FormatterConfig formatterConfig) {
        Integer originRadix = BeanManager.getConversionService()
            .convert(formatterConfig.getConfigParams().get(CommonParameterDefs.ORIGIN_RADIX.getKey()), Integer.class);
        Integer targetRadix = BeanManager.getConversionService()
            .convert(formatterConfig.getConfigParams().get(CommonParameterDefs.TARGET_RADIX.getKey()), Integer.class);
        long originalNumber = Long.parseLong(text, originRadix);
        if (NumberRadixValues.RADIX34.getValue().equals(targetRadix)) {
            return NumberRadixUtils.to34(originalNumber);
        } else {
            return Long.toString(originalNumber, targetRadix);
        }
    }
}
  • 在META-INF/spring.factories添加如下语句

spring.factories
com.ags.lumosframework.common.formatter.IFormatter=\
com.ags.lumosframework.common.formatter.NumberRadixFormatter,\
com.ags.lumosframework.common.formatter.LeftRightPadFormatter

1.13. FreeMarker模板管理

  • FreeMarker 是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。可参考官方教程http://freemarker.foofun.cn/toc.html

  • lumos 提供了FreeMarker模板的管理功能,根据模板名字和自定义参数解析模板和HTML转PDF的功能。

1.13.1. 自定义一个模板举例

HTML模板的定义
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"></meta>
    <title>Title</title>
</head>
<body>
    <p> (1)
        ${parameter1}  这是一个测试模板 ${parameter2}
    </p>
    <p> (2)
        <img src="/9j/4AAQSkZJRgABAQEAYABgAAD/4RCORXhpZgAATU0AKgAAAAgABAE7AAIAAAAJAAAISodpAAQAAAABAAAIVJydAAEAAAASAAAQdOocAAcAAAgMAAAAPgAAAAAc6gAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHRyYWN5X2d1AAAAAeocAAcAAAgMAAAIZgAAAAAc6gAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdAByAGEAYwB5AF8AZwB1AAAA/+EKYWh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8APD94cGFja2V0IGJlZ2luPSfvu78nIGlkPSdXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQnPz4NCjx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iPjxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+PHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9InV1aWQ6ZmFmNWJkZDUtYmEzZC0xMWRhLWFkMzEtZDMzZDc1MTgyZjFiIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iLz48cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0idXVpZDpmYWY1YmRkNS1iYTNkLTExZGEtYWQzMS1kMzNkNzUxODJmMWIiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyI+PGRjOmNyZWF0b3I+PHJkZjpTZXEgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj48cmRmOmxpPnRyYWN5X2d1PC9yZGY6bGk+PC9yZGY6U2VxPg0KCQkJPC9kYzpjcmVhdG9yPjwvcmRmOkRlc2NyaXB0aW9uPjwvcmRmOlJERj48L3g6eG1wbWV0YT4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0ndyc/Pv/bAEMABwUFBgUEBwYFBggHBwgKEQsKCQkKFQ8QDBEYFRoZGBUYFxseJyEbHSUdFxgiLiIlKCkrLCsaIC8zLyoyJyorKv/bAEMBBwgICgkKFAsLFCocGBwqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKv/AABEIACgAXgMBIgACEQEDEQH/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/APpGiiuU8V/EXRPCcn2e5d7q9xn7NBglfTcTwv8AP2q4U5VJcsFdkynGCvJ2OroryI/HR92//hHG+z5xu+1c/wDoGK6Xxx47u/DXhbTdQtLOJbu+Zc29zlvLXZuYHaRyCQK6Hg60ZKLWrMViaTi5J7HcUVj+E9TvNZ8K2Go6kkUdxdR+YVhBCgEnbjJJ+7itdjtUnBOBnA71yzXI2n0N4vmSaForM0vVZdQnlSS38tUGQw7c9D71p1yYXFUsXSVai7xflbb1NKlOVOXLLcKKKK6iAoorG1LXXsbtoFtwduDuZuorjxmNoYKn7Wu7K9tm/wAjSnSlVlyxH+KtYOg+FdQ1NADJbwkxg9N54XPtkivJPhN4Yh8S6vfa5rq/bFt3G1ZvmEkrclmz1x6H19q9V8aaPLr3gzUtOtuZposxj+8ykMB+JUCvH/hn45tvB1xe6brsUsVvM4beEJaGQcEMvXHT3BHTnj6HCqTw0/Z/F+Nv6ueXiGlXhz/D+p7z5UfleV5a+XjGzbxj0xXifxzv/N8QabYA5FvbGUj0Ltj+SCvSNL+IfhvWtYt9M0u+a4ubjdsAhdRwpY5LAdga8w1ML4m+Pq27L5kMV4kZQ8grCuWH5q351OCpyp1XKatZNjxU4zpqMHe7sSeINf8AH3h3StMu2KaRpzIkNtaxhGKBVGA+4E5IHf8AIV3dz4wux8Hz4k+WC9e2G0hQQJC+zIB9+cVynx2v8y6Rp6noJJ3H1wq/yapPiSf7E+FWgaKvyvJ5YkHrsTLf+PEVvyRqxpNxSbf4Iy5pU5VEm2kvxNbwL4q1u+8Ba1r2u3gnNt5nkExImNke4/dAzkkDn0rm/Cvjfx54ljvLDTGjurshW+1SxoiWyDOegALMcdc9Dx1IvXh/sD9neCL7st+F59fMff8A+gDFa/wgtYtK+Htxqkq48+WSZn7lEGMfgQ350pezjCpU5V8Vlp2HHnlOEOZ7XZjfDzxv4kufHLaD4guPtQbzUYOqhoZEBJ5Ucj5SK2fHPxLn0nVxoHhm2F1qbFUdyu4IzdEVR95uR7D35xyvwbgfU/HWoarP8zRwO5P+3Iw/purJ8HatZ6T8VJ7/AMSyeSVln3SOpPlykkZOOfUfjWs6FN1pPl+FLRdWZxrTVKKvu9/I0fE+vfETwo1pLrGtRpJdhmSGIRts24zkBcd/fvXr3h5Jb7w5p13rKxz301sjyuYwOWG7GPbNeIePteTxp41s/sSyCwOy2tpHUr5uXwzgHtk4/wCA+vFfQixhIlji+RVAAAHQDtXFmFODowU4K712R04ST9pNxk7LzH1i6v4O8P69N52q6VBPMRgy4KOfqykE0UVwRlKLvF2O2UVJWaItK8DeG9EvEu9M0qKG4jzskLM7LkY4LE44NTWfhLQtP1dtUs9NiivWZmM4JLZb7x5PfJooqnVqPeT+8lU4LZBqvhLQtcvVu9W02K6nVAgdyeFBJA6+pNS6z4a0fxA0J1mxjuzCCI95Py5xnofYUUUlUmrWb0HyRd9Nwv8Aw1o+qabb6ff2Mc1pa7fJhJIVMDaMYPYcVNDounW+inSILVEsDG0RgXO0q2dw9ecn86KKXPK1rhyxvexFo3hvSPD/AJ39jWMdp5+PM2Z+bGcdT7mqep+BvDWs6gb7UdJhmuWxuk3Mu76hSAfxooqlUmpcybuLkg1y20JbjwfoF1d21zPpcDS2iIkBGVEaqcqAAcAAmtqiipcpS3ZSilsj/9k="/>
    </P>
</body>
</html>
<1> parameter1和parameter2 为模板中的变量,必须放在${}里面,可以支持多个层级,比如${root.parameter}具体可以参考官网。
<2>支持存放图片,但只限于Base64转换后的图片。这里需要注意的是,转换后的头信息不能写入img src里面,见如下图红框所示:
freemarkerbase64

1.13.2. 上传模板

提供上传和下载模板,模板的名字必须唯一性(根据这个名字来唯一识别一个模板)

freemarkerpage
freemarkeradd
IFreeMarkTemplateHandler接口提供了模板解析,PDF转换等功能,注入该类直接使用。

1.13.3. 解析模板

根据上传模板的名字和自定义参数,生成对应的文件(HTML网页,电子邮件,配置文件,源代码等)

IFreeMarkTemplateHandler
public interface IFreeMarkTemplateHandler extends IBaseEntityHandler<FreeMarkerTemplateEntity> {

    /**
     * FreeMarker模板解析
     * @param name 模板名字(1)
     * @param root 模板数据模型(2)
     * @return 模板解析后的字符串(3)
     */
    String generateTemplate(String name, Map<String , Object> root) throws IOException, TemplateException;
}
<1> 我们维护的模板名字
<2> 传入的参数,Map的value可以是任意类型,也可以是个bean(详情见官网)
<3> 返回解析成功后的String

1.13.4. 转成PDF

根据上传模板的名字和自定义参数,生成对应的PDF文件

IFreeMarkTemplateHandler
public interface IFreeMarkTemplateHandler extends IBaseEntityHandler<FreeMarkerTemplateEntity> {

       /**
     *
     * @param name 模板名字
     * @param root 模板数据模型
     * @return pdf字节流
     */
    byte[] generatePdfWithHtmlTemplateByName(String name, Map<String , Object> root) throws IOException, TemplateException, DocumentException;
}
lumos只提供了HTML模板转成PDF的功能

1.13.5. 举例

此处用上面提到的模板来生成对应的PDF文件

public class Test{
    private void freeMarkSample(long objId){
        FreeMarkerTemplateEntity entity = freeMarkTemplateHandler.getById(objId);
        Map<String, Object> params = new HashMap<>();
        params.put("parameter1","Hello:");
        params.put("parameter2","Welcome");
        try(FileOutputStream fileOutputStream = new FileOutputStream( "D:\\testPdf.pdf");) {
            byte[] result = freeMarkTemplateHandler.generatePdfWithHtmlTemplateByName(entity.getName(), params);
            fileOutputStream.write(result);
        } catch (Exception e) {
           e.printStackTrace();
        }
    }
 }

这就是最后生成的PDF文件,支持中英文和图片

freemarkerresult

1.14. 企业微信发送应用消息

  • 通过调用接口发送微信应用信息给某个或某些人,目前仅支持图片或文本。

1.14.1. 配置微信企业ID,应用ID等

application.properties配置文件
lumos.wechat.corp-id=ww4129938db808b721 (1)
lumos.wechat.corp-secret=HnNE5kAvT6bWmTFBxvtCPRSNh7-zzECd1EsYu9-euto (2)
lumos.wechat.agent-id=1000003 (3)
1 每个企业都拥有唯一的corpid
2 secret是企业应用里面用于保障数据安全的“钥匙”,每一个应用都有一个独立的访问密钥
3 每个应用都有唯一的agentid

1.14.2. 用户表添加企业微信号

用户页面可以填写企业微信号, 默认是不显示。 如果需要显示可在: 高级设定- 页面设置- 选择用户 - 设置表单 把“WeChat Account”勾上显示。

wechatuser

1.14.3. 微信Token缓存配置

application.properties配置文件
lumos.commons.ehcache3.cache-config[weChatCache].cache-heap-size=2 (1)
lumos.commons.ehcache3.cache-config[weChatCache].cache-off-heap-size=20 (2)
lumos.commons.ehcache3.cache-config[weChatCache].time_to_live=1.5 (3)
1 微信缓存在本地堆内存的大小为2M
2 微信缓存在本地堆外内存的大小为20M
3 微信缓存失效时间1.5小时
3项同时配置才生效。也可以不配置采用系统中默认的缓存配置。 由于微信端token的有效时间是2小时, 建议配置, 且失效时间稍少于2小时。

1.14.4. 调用发送应用消息API

调用相应API可以发送图片或是文本。 不支持同时包含文本和图片。

IWeChatService
public interface IWeChatService {
    /**
     * 微信发生应用图片,图片不能超过2MB,支持JPG,PNG格式
     *
     * @param fileName 图片名
     * @param bytes    图片字节流
     * @param sendTo   发给谁(微信企业号)
     * @return
     * @throws Exception
     */
    SendMsgResponseBody sendPictureToUsers(String fileName, byte[] bytes, String... sendTo) throws Exception;

    /**
     * 微信发生应用文本消息
     *
     * @param content 发生文本内容 不能超过2048个字节 否则被截掉
     * @param sendTo  发给谁(微信企业号)
     * @return
     * @throws Exception
     */
    SendMsgResponseBody sendTextToUsers(String content, String... sendTo) throws Exception;
}

1.15. 数据字典

平台具有数据字典功能,运行其他应用以及项目直接引用,对数据字段进行扩展。 数据字典的使用场景,主要在于对数据类型的扩展,可以省去很多基础数据表的增删改查。

比如场景1,在实现某项目中,需要对客户的产品不良提供分类,如果使用传统方法,则需要创建不良分类的基础表,然后提供增删改查页面,比较麻烦。
如果借助于数据字典,该不良分类仅仅是字典里面的一条记录,然后在该记录下以键值对的形式提供扩展,实施者可以收集这些值放入Combobox等控件中。
在二次开发中比较好用。
..\images\core\data dictionary
具体调用的service接口类:IDataDictionaryService ,实现类:DataDictionaryService。

1.16. 扩展系统菜单

Lumos脚手架中,默认提供一个应用:管理项。 其他的应用可以通过扩展的方式加入系统。

1.16.1. 扩展系统应用

如果要扩展系统的应用,只需要借助于如下一个注解即可。

系统在启动时,会自动加载所有被该注解标识的UI,自动解析出应用信息放入系统缓存备用
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface WebEntry {

    /**
     * 应用所属的应用分组,如管理相关,运行期相关等,可以为空
     *
     * @return
     */
    String groupName() default "";

    /**
     * 在首页显示时使用的icon路径,该路径可以为网络路径,也可以是项目中的相对路径
     *
     * @return
     */
    String iconPath() default "";

    /**
     * 在首页显示时使用的icon路径,该路径可以为网络路径,也可以是项目中的相对路径
     *
     * @return
     */
    String[] backgroundPath() default {"platform/img/index/bg1.png", "platform/img/index/bg2.png"};

    /**
     * 在首页显示时的css样式信息
     *
     * @return
     */
    String cssClass() default "";

    /**
     * 首页显示该应用时的标题信息
     *
     * @return
     */
    String shortCaption() default "";

    /**
     * 首页显示时标题的国际化信息
     *
     * @return
     */
    String shortCaptionI18NKey() default "";

    /**
     * 首页显示该应用的时候,可以显示一段描述
     *
     * @return
     */
    String longCaption() default "";

    /**
     * 首页显示该应用的时候,可以显示一段描述的国际化信息
     *
     * @return
     */
    String longCaptionI18NKey() default "";

    /**
     * 首页显示该应用的时候,可以显示一段描述
     *
     * @return
     */
    String description() default "";

    /**
     * 首页显示该应用的时候,可以显示一段描述的国际化信息
     *
     * @return
     */
    String descriptionI18NKey() default "";

    /**
     * 显示在首页的排序,数值越小,优先级越高,反之亦然
     *
     * @return
     */
    int order() default 0;

    /**
     * 如果该应用在进入页面后有菜单,并且菜单有分组信息,那么可以预先加载分组信息.
     * <p>
     * 这个分组信息也可以不在此时定义,后续依然可以通过扩展接口的方式实现
     *
     * @return
     */
    MenuGroup[] viewGroups() default {};
}
MenuGroup定义
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface MenuGroup {

    /**
     * 应用侧边栏分组信息的名称
     *
     * @return
     */
    String name();

    /**
     * 应用侧边栏的标题
     *
     * @return
     */
    String caption();

    /**
     * 应用侧边栏的标题的国际化信息
     *
     * @return
     */
    String captionI18NKey() default "";

    /**
     * 如果定义icon,那么该icon将显示在菜单分组信息的左侧
     *
     * @return
     */
    String iconPath();

    /**
     * 该菜单分组的排序,数值越小,越靠前
     *
     * @return
     */
    int order();

    /**
     * 三级菜单支持,表示一个组下面继续挂组
     *
     * @return
     */
    MenuChildGroup[] childGroups() default {};
}
MenuChildGroup定义
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface MenuChildGroup {

    /**
     * 应用侧边栏分组信息的名称
     *
     * @return
     */
    String name();

    /**
     * 应用侧边栏的标题
     *
     * @return
     */
    String caption();

    /**
     * 应用侧边栏的标题的国际化信息
     *
     * @return
     */
    String captionI18NKey() default "";

    /**
     * 如果定义icon,那么该icon将显示在菜单分组信息的左侧
     *
     * @return
     */
    String iconPath();

    /**
     * 该菜单分组的排序,数值越小,越靠前
     *
     * @return
     */
    int order();
}

如下是一个简单的样例实现,供参考:

@WebEntry(longCaption = "管理项", shortCaption = "Administration", shortCaptionI18NKey = "ui.administration",
        description = "For enterprise IT managers to perform system level management.",
        descriptionI18NKey = "Home.Page.Administration.Description", iconPath = "platform/img/index/icon_Administration.png",
        backgroundPath = {"platform/img/index/Administration1.png", "platform/img/index/Administration2.png"}, order = 400,
        viewGroups = {@MenuGroup(name = LumosConstants.ADMIN_OC_RELATED_Default, // oc相关
                captionI18NKey = LumosConstants.ADMIN_OC_RELATED_I18N, caption = LumosConstants.ADMIN_OC_RELATED_Default,
                iconPath = "", order = 0),
                @MenuGroup(name = LumosConstants.ADMIN_ADVANCED_SETTING_Default, // 高级设定
                        captionI18NKey = LumosConstants.ADMIN_ADVANCED_SETTING_I18N,
                        caption = LumosConstants.ADMIN_ADVANCED_SETTING_Default, iconPath = "", order = 1,
                        childGroups = {
                                @MenuChildGroup(name = "", caption = "", captionI18NKey = "", iconPath = "", order = 1),
                                @MenuChildGroup(name = "", caption = "", captionI18NKey = "", iconPath = "", order = 1)
                        })})
@SpringUI(path = "administration")
@Secured(LumosPermissionConstants.ADMINISTRATION_MANAGE)
@Theme("light")
@Push(transport = Transport.WEBSOCKET_XHR)
public class AdminUI extends BaseUIHasMenu {

    /**
     *
     */

}

1.16.2. 扩展或添加系统应用页面

实施人员可以很方便的向既有的应用中添加一个页面,只需要实现如下的注解即可。

系统在启动时,会遍历系统中所有该注解标注的页面,放入系统缓存,等用户访问页面时,将根据这些页面信息,自动生成相应的菜单,在菜单中提供了通向这些页面的链接。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Menu {

    /**
     * 如果指定,将在把应用放在菜单时,自动寻找菜单分组放入
     *
     * @return
     */
    String groupName() default "";

    /**
     * 应用在菜单上显示的名称
     *
     * @return
     */
    String caption();

    /**
     * 应用在菜单上显示的名称的国际化信息
     *
     * @return
     */
    String captionI18NKey() default "";

    /**
     * 如果定义,将在菜单左侧显示icon
     *
     * @return
     */
    String iconPath();

    /**
     * 应用在菜单分组时的排序信息
     *
     * @return
     */
    int order();
}

如下是系统中用户界面的简单实现,供大家参考:

@Menu(groupName = LumosConstants.ADMIN_OC_RELATED_Default, caption = "user", captionI18NKey = "admin.user",
    iconPath = "images/icon/user.png", order = 0)
@SpringView(name = "user", ui = AdminUI.class)
@Secured(LumosPermissionConstants.USER_MANEGE)
public class UserView extends BaseView implements Button.ClickListener, IFilterableView {

}

1.16.3. UI中加入自定义的菜单

实施人员可以添加自己定义的菜单,然后在指定的UI中显示

例如:我们在管理项中创建一个新的报表

report

如图所示我们在报表页面可以看到新创建的报表,它根据创建的category,显示在对应的Group下

report menu

具体实现代码如下

@WebEntry(longCaption = "报表", shortCaption = "Report", shortCaptionI18NKey = "Common.Report",
    description = "Visual Display and Individual Formulation of Data Statistical Statements.",
    descriptionI18NKey = "Home.Page.Report.Description", iconPath = "platform/img/index//icon_Report.png",
    backgroundPath = {"platform/img/index/Report1.png", "platform/img/index/Report2.png"}, order = 500)
@SpringUI(path = CommonConstants.REPORT)
@Secured(LumosPermissionConstants.REPORT_MANAGE1)
@Theme("light")
public class ReportUI extends BaseUIHasMenu {

    private static final long serialVersionUID = -552520960746758358L;

    private SortedMap<ApplicationPageGroup, SortedSet<ApplicationPageInfo>> defaultMenuGroups = new TreeMap<>();

    @Override
    protected void setTitle() {
        Page.getCurrent().setTitle(I18NUtility.getValue("Common.Report", "Report"));
    }

    @Override
    protected void initSideBar() {

    }

    @Override
    protected void init(VaadinRequest request) {
        super.init(request);
        addExtendMenuGroup();
    }

    private void addExtendMenuGroup() {
        ApplicationInfo appInfoByAppPath = ApplicationManager.getAppInfoByAppPath(CommonConstants.REPORT);
        SortedSet<ApplicationPageGroup> groups = ApplicationManager.getAppPageGroupsByApp(appInfoByAppPath);
        for (ApplicationPageGroup group : groups) {
            SortedSet<ApplicationPageInfo> pageInfos = ApplicationManager.getAppPageInfosByGroup(group);
            defaultMenuGroups.put(group, pageInfos);
        }

        // 用户自定义
        List<Report> list = BeanManager.getService(IReportService.class).list(0, Integer.MAX_VALUE);
        Collections.sort(list, new Comparator<Report>() {

            @Override
            public int compare(Report arg0, Report arg1) {
                return arg0.getOrder() - arg1.getOrder();
            }
        });

        int flag = 4;
        for (Report report : list) {
            String menuName = getMenuName(report);
            if (findDefaultPageGroup(menuName) == null) {
                String captionI18N = report.getCategoryI18N() == null ? "" : report.getCategoryI18N().getName();
                ApplicationPageGroup build = new ApplicationPageGroup(menuName, report.getCategory(), captionI18N, null,
                    ++flag, true, appInfoByAppPath);
                defaultMenuGroups.put(build, new TreeSet<ApplicationPageInfo>());
            }
        }

        for (Report report : list) {
            ApplicationPageGroup apg1 = findDefaultPageGroup(getMenuName(report));
            if (apg1 != null) {
                SecuredInfo si = new SecuredInfo();
                Permission privilege = report.getPrivilege();
                si.setRequiredPermission(privilege == null ? null : privilege.getName());
                I18NText menuCaptionI18N = report.getMenuCaptionI18N();
                String menuCaptionI18NId =
                    menuCaptionI18N == null ? report.getMenuCaption() : menuCaptionI18N.getName();
                ApplicationPageInfo pageInfo = new ApplicationPageInfo();
                pageInfo.setCaption(report.getMenuCaption());
                pageInfo.setCaptionI18NKey(menuCaptionI18NId);
                pageInfo.setApplicationInfo(appInfoByAppPath);
                pageInfo.setGroup(apg1);
                pageInfo.setName(IBirtReportView.NAME + "/i18n=" + menuCaptionI18NId + ":" + report.getMenuCaption()
                    + "&" + IBirtReportView.REPORT_URL_KEY + "=" + report.getId());
                pageInfo.setOrder(report.getOrder());
                pageInfo.setSecuredInfo(si);
                defaultMenuGroups.get(apg1).add(pageInfo);
            }
        }
        addPageGroup(defaultMenuGroups);
    }

    private String getMenuName(Report report) {
        I18NText text = report.getCategoryI18N();
        if (text != null) {
            return BeanManager.getService(I18NTextItemService.class)
                .getByLocale(report.getCategoryI18N(), RequestInfo.current().getUserLocal()).getValue();
        } else {
            return report.getCategory();
        }
    }

    private ApplicationPageGroup findDefaultPageGroup(String name) {
        for (ApplicationPageGroup apg : defaultMenuGroups.keySet()) {
            if (apg.getCaptionI18NKey() != null) {
                String caption = I18NUtility.getValue(apg.getCaptionI18NKey());
                caption = caption == null ? apg.getCaption() : caption;
                if (caption.equals(name)) {
                    return apg;
                }
            } else {
                if (apg.getName().contentEquals(name)) {
                    return apg;
                }
            }
        }
        return null;
    }

}

1.16.4. 自定义TabSheet显示Caption

在上述实现方法通过将自定义的页面信息封装成ApplicationPageGroup 对象添加到 ApplicationInfo中; 系统在启动时,自动生成相应的菜单,选中一个View时,会调用相关方法,将view添加到Tab中,展示该caption。

如果我们要自定义一个view,就需要在ViewChangeEvent事件中添加相关参数。同样,也会将此view添加到Tab中,参数名称即作为tab的caption显示。

例如WMS中选中工单查看,对应参数的添加
UI.getCurrent().getNavigator().navigateTo(IOrderDetailView.NAME + "/" + IOrderDetailView.ORDER_NO
       + "=" + orderHeader.getOrderNo() +"&" + IOrderDetailView.VIEW_TYPE + "=" + ViewType.VIEW);

第一个参数的Value,会作为它的caption,例如上述代码中的caption为 orderHeader.getOrderNo()

最后解析成页面展示的caption,如下图

customized caption

1.16.5. TabSheet隐藏或展现

在登入账号-设置-偏好设置里面可以设置Tab展现与否

tabsheetDisplay

1.16.6. 扩展系统应用或页面 — 高级篇

如果如上方法不能满足要求,比如说其他非Vaadin页面的应用嵌入,或者说Lumos应用下的扩展菜单的实现,那么此时你需要了解Lumos的应用加载策略。

在Lumos中,系统在启动时,会根据如下接口找寻系统中的所有应用信息:

/**
 * 用于扩展系统中菜单
 * <p>
 * 仍然可以利用@WebEntry向菜单中加入菜单分组信息等,该方式与其不冲突。
 * </tr>
 * 整个流程分为4个阶段,分别加载应用组,应用,菜单组,菜单等信息。方法的执行会按照顺序加载,即先加载所有实现类的ApplicationGroup,然后再加载所有实现类的Application。
 * </tr>
 * 所以该接口的扩展类可以仅仅选择实现这些接口里面的某些方法。
 * </tr>
 * 比如,如果需要向AdminUI里面加入定制化的某一个View,这个View不属于当前任何一个分组,那么实现者只需要实现loadApplicationPageGroup() 和loadApplicationPageInfo()即可
 *
 * @author yuri_li
 * @date 2019/08/01
 */
public interface IApplicationInfoLoader {

    /**
     * 向页面中心注册当前页面应用组的信息,该应用组表示应用所属应用的大类,如应用可以分为“运行期应用”,“编译期应用”,“管理相关应用”
     */
    void loadApplicationGroup();

    /**
     * 加载应用的信息,如工单,对象建模等模块,分属于ApplicationGroup
     */
    void loadApplication();

    /**
     * 加载应用信息里面的菜单分组信息,如建模里面的“工厂建模”,“人力资源”等菜单分页信息,分属于Application
     */
    void loadApplicationPageGroup();

    /**
     * 加载具体的页面信息,分属于ApplicationPageGroup
     */
    void loadApplicationPageInfo();

    /**
     * 返回例外的ApplicationGroup,在菜单页里不会加载出来
     * <p>注意该方法会在loadApplicationGroup之前调用,需要使用者构建ApplicationGroup对象,然后返回</p>
     *
     * @return
     */
    default Set<ApplicationGroup> getExcludedApplicationGroups() {
        return new HashSet<>();
    }

    /**
     * 返回例外的ApplicationInfo,在菜单页里不会加载出来
     * <p>注意该方法会在loadApplication之前调用,需要使用者构建ApplicationInfo对象,然后返回</p>
     *
     * @return
     */
    default Set<ApplicationInfo> getExcludedApplicationInfos() {
        return new HashSet<>();
    }

    /**
     * 返回例外的ApplicationPageGroup,在菜单页里不会加载出来
     * <p>注意该方法会在loadApplicationPageGroup之前调用,需要使用者构建ApplicationPageGroup对象,然后返回</p>
     *
     * @return
     */
    default Set<ApplicationPageGroup> getExcludedApplicationPageGroups() {
        return new HashSet<>();
    }

    /**
     * 返回例外的ApplicationPageInfo,在菜单页里不会加载出来
     * <p>注意该方法会在loadApplicationPageInfo之前调用,需要使用者构建ApplicationPageInfo对象,然后返回</p>
     *
     * @return
     */
    default Set<ApplicationPageInfo> getExcludedApplicationPageInfos() {
        return new HashSet<>();
    }

    /**
     * 返回加载顺序
     *
     * @return
     */
    default int getOrder() {
        return 0;
    }

}

正如如上接口声明的那样,整个应用的加载分为多个阶段,每个阶段独立运行,互不影响。 所有该接口实现类里面的loadApplicationGroup方法一定优先于任何一个实现类的loadApplication()执行。
所以,如果你需要扩展系统应用,可以选择实现该接口里面的一个方法即可。

getExcluded 方法用于添加一些不想在页面显示的菜单页面,或菜单组,应用

getOrder 方法loader 加载顺序定制,当需要在原有菜单组里添加一个新的组时,可以在loadApplicationPageGroup 方法内部添加新的组信息

可以参考如下实现,向Lumos系统中添加了额外的三级菜单。

@Component
public class DemoApplicationLoader implements IApplicationInfoLoader {

    @Override
    public void loadApplicationGroup() {

    }

    @Override
    public void loadApplication() {

    }

    @Override
    public void loadApplicationPageGroup() {
       //获取应用信息
        ApplicationInfo appInfo = ApplicationManager.getAppInfoByAppPath(CommonConstants.REPORT);
        //获取应用下平台已经加载的组
        ApplicationPageGroup demo = ApplicationManager.getAppPageGroupByAppAndGroupName(appInfo, "demo");
        //添加子组(三级菜单功能)
        ApplicationPageGroup subDemo =
                new ApplicationPageGroup("subDemo", "caption",
                        "caption_i18n", "images/icon/report/QualityReport.png", 4, appInfo);
        subDemo.setParentApplicationPageGroup(demo);
        ApplicationManager.addApplicationPageGroup(subDemo);
    }

    @Override
    public void loadApplicationPageInfo() {

    }
}

当然,你也可以选择仅仅加入页面信息。

一定确保在具体的实现中加入对应目的的代码,比如在loadApplication中最好不要加载页面的信息,否则可能引起其他未知错误。

1.17. 在帮助页面新增资源文件

1.17.1. 实现原理

新建类实现Link控件信息接口,通过@Component注解让该类在View中可以被发现,然后获取其中的值生成Link控件

1.17.2. 实现流程

新建一个类,实现 IHelpItemInfo 接口并重写方法,并加上@Component注解

例如:

JavaApiDocLumos.class
package com.ags.lumosframework.web.vaadin.ui.help;
import com.ags.lumosframework.web.common.functionality.IHelpItemInfo;
import org.springframework.stereotype.Component;
@Component
public class JavaApiDocLumos implements IHelpItemInfo {

    //返回Link控件的标题
    @Override
    public String getCaption() {
        String caption = "JavaApiDoc-lumos"
        return caption;
    }

    //返回Link控件的链接地址
    @Override
    public String getPath() {
        String path = "help/apidocs-lumos/index.html"
        return path;
    }

    //返回控件位置优先级,越小优先级越高
    @Override
    public int getShowOrder() {
        int order = 1
        return order;
    }
}

1.18. 延迟加载页面数据

在项目开发中,对于复杂的页面数据加载往往需要较长的时间完成,如果等所有数据加载完后页面再弹出来,那么会给使用者造成一种假象:我们的系统非常的慢,性能不高。所以为了更好的用户体验,需要将页面首先进行部分渲染,然后将数据再分步加载,以获得良好的用户体验。

为此,平台提供了一套延迟加载的机制,允许开发人员将数据加载操作放入后端执行,并且不会阻塞前端的界面的渲染进程。

1.18.1. 设计细节

平台会将加入后端执行的耗时操作,按照调用的顺序,按照如下规则,依次执行:

  • 不同的组件中的后端任务,无序独立被执行。例如UserView和AddUserDialog,两者不能严格保证他们的调用顺序。

  • 同一个组件中后端任务,会按照被调用顺序,依次执行。例如AddUserDialog中,initUIData、setObject、show等方法,按照加入后端任务的顺序,依次执行。

在所有的前端组件的基础类中,有两个方法供大家使用:

com.ags.lumosframework.web.vaadin.base.BaseComponent
/**
     * 将页面渲染的操作放入后台运行,不会阻塞当前主线程的运行,常常用于页面分开渲染。
     * <tr>
     * 该方法支持嵌套,如果当前逻辑已经在后台运行,那么调用该方法将不会再次放入后台运行,而会立即执行
     *
     * @param loadData
     *            要被渲染的具体任务
     */
    protected void pushToRenderQueue(ILoadData loadData) {
        UIRender.addRenderItem(new UIRenderItem(UI.getCurrent(), this, loadData));
    }

    /**
     * 将页面渲染的操作放入后台运行,不会阻塞当前主线程的运行,常常用于页面分开渲染。
     * <tr>
     * 该方法支持嵌套,如果当前逻辑已经在后台运行,那么调用该方法将不会再次放入后台运行,而会立即执行
     *
     * @param owner
     *            表示当前的数据加载时属于哪个组件的;相同Owner的,会保证按照顺序逐一执行。
     * @param loadData
     *            要被渲染的具体任务
     */
    protected void pushToRenderQueue(Object owner, ILoadData loadData) {
        UIRender.addRenderItem(new UIRenderItem(UI.getCurrent(), owner, loadData));
    }

值得说明的是,不单单UI的数据加载操作可以放入后端,使用者可以将任何操作放入后端,只需要调用如下方法即可:

com.ags.lumosframework.web.vaadin.base.UIRender
public class UIRender {
    /**
     * 将页面渲染的操作放入后台运行,不会阻塞当前主线程的运行,常常用于页面分开渲染。
     * <tr>
     * 该方法支持嵌套,如果当前逻辑已经在后台运行,那么调用该方法将不会再次放入后台运行,而会立即执行
     *
     * @param renderItem
     *            要被渲染的具体任务
     */
    public static void addRenderItem(UIRenderItem renderItem) {
        //省略代码实现细节
    }
}

1.18.2. 使用示例

在用户界面中,有三处地方将数据加载操作放入了后端处理

  • 初始化方法(init)中,这个方法加入后端处理的原因在于会去获取用户扩展的tab,比较耗时。

  • 进入页面的方法(enter)中,在进入页面的时候,会去后端加载数据。

  • 当用户点击一个用户的时候,向不同的Tab设置被点击的用户数据。

    private void setSubDataForUser(User item) {

        //如下操作放入后端处理处理。
        pushToRenderQueue(renderItem -> {
            if (item instanceof User) {
                roleGrid.setDataProvider(DataProvider.fromStream(item.getRole().stream()));
                if (dataPermissionTab != null) {
                    this.dataPermissionTab.setUser(item);
                }
                if (workScheduleTab != null) {
                    this.workScheduleTab.setObject(item, assginWorkSchedulePermissionCode);
                }
                userExtendedTabs.forEach(userExtendedTab -> userExtendedTab.setUser(item));
            } else {
                roleGrid.setDataProvider(DataProvider.ofCollection(new ArrayList<Role>()));
            }
        });

        //每个Tab的数据的刷新,如果需要放入后端,则是由每个页面负责。
        superPermissionTab.refresh(item);
        userPermissionShowTab.refresh(item);
        userSkillTab.refresh(item);

    }

1.19. 系统提供的UI组件

1.19.1. 分页组件

目前,平台提供两种分页组件,一种是适用于任何对象的 PaginationObjectListGrid 分页组件,内部具体属性和方法如下:

/**
 * 可以绑定任何对象,如果需要使用单表对象,建议参考{PaginationDomainObjectList}
 */
public class PaginationObjectListGrid<T> extends BaseView implements IObjectListGrid<T>, ClickListener {

    /**
     *
     */
    private static final long serialVersionUID = -3903777668282783935L;
    public static int LOADING_DATA_SIZE = 30;
    public Set<String> columnNameList = new HashSet<String>();
    protected Grid<T> grid = new Grid<>();
    List<GridSortOrder<T>> sortOrder = Collections.emptyList();
    private IObjectClickListener<T> objectClickListener = null;
    private IObjectSelectionListener<T> objectSelectionListener = null;
    private List<T> data = null;
    private boolean isLocalModel = false;
    @SuppressWarnings("unused")
    private boolean isDialog = false;

    private Label lblPageInfo = new Label();
    private Button btnCurrentPage = new Button();
    private Button btnFirstPage = new Button();
    private Button btnLastPage = new Button();
    private Button btnPreviousPage = new Button();
    private Button btnNextPage = new Button();
    private Button[] btns = {btnFirstPage, btnLastPage, btnPreviousPage, btnNextPage};
    private int currentPage = 1;
    private int pageSize = LOADING_DATA_SIZE;
    private int totalCount = -1;
    private int startPosition = 0;

    protected IPagingQuery<T> pagingQuery = null;

    // 该缓存用于存储每页用户选择的数据,当用户翻页时,会将数据放入该缓存,当用户回退到该页时,也会根据该缓存的值再次选中
    private Map<Integer, Set<T>> selectedCache = new HashMap<Integer, Set<T>>();

    // 这个对象存储打开这个grid的时候,外部传入的需要选中的对象,或者变更查询条件的时候,上次的选中对象
    private Set<T> selectedPassedByOutside = new HashSet<>();

    public PaginationObjectListGrid(IPagingQuery<T> queryFilter) {
        pagingQuery = queryFilter;
        initGrid();
        VerticalLayout vlRoot = new VerticalLayout();
        vlRoot.addStyleName(MaterialTheme.CARD_0 + " " + MaterialTheme.CARD_NO_PADDING);
        vlRoot.setMargin(false);
        vlRoot.setSpacing(false);
        vlRoot.setSizeFull();
        grid.setSizeFull();
        vlRoot.addComponent(grid);
        vlRoot.setExpandRatio(grid, 1);
        vlRoot.setId("vl_root");
        setElementsId();

        HorizontalLayout hlTool = new HorizontalLayout();
        hlTool.setSpacing(false);
        hlTool.setMargin(new MarginInfo(true, false, false, false));
        hlTool.setDefaultComponentAlignment(Alignment.MIDDLE_RIGHT);
        hlTool.setWidth("100%");
        HorizontalLayout hlMsg = new HorizontalLayout();
        hlMsg.setWidth("100%");
        hlMsg.addComponent(lblPageInfo);
        hlTool.addComponents(hlMsg, btnFirstPage, btnPreviousPage, btnCurrentPage, btnNextPage, btnLastPage);
        hlTool.setExpandRatio(hlMsg, 1);

        btnCurrentPage.setEnabled(false);
        btnPreviousPage.setIcon(VaadinIcons.ANGLE_LEFT);
        btnNextPage.setIcon(VaadinIcons.ANGLE_RIGHT);
        btnFirstPage.setIcon(VaadinIcons.ANGLE_DOUBLE_LEFT);
        btnLastPage.setIcon(VaadinIcons.ANGLE_DOUBLE_RIGHT);
        for (Button btn : btns) {
            btn.setDisableOnClick(true);
            btn.addClickListener(this);
            btn.setHeight("24px");
            btn.addStyleNames(MaterialTheme.BUTTON_BORDERLESS, MaterialTheme.BUTTON_ICON_ONLY,
                CoreTheme.BUTTON_PAGEINFO);
        }
        btnCurrentPage.setHeight("24px");
        btnCurrentPage.addStyleNames(MaterialTheme.BUTTON_BORDERLESS, MaterialTheme.BUTTON_ICON_ONLY,
            CoreTheme.BUTTON_PAGEINFO_CURRENT);
        vlRoot.addComponent(hlTool);
        vlRoot.setComponentAlignment(hlTool, Alignment.MIDDLE_RIGHT);
        this.setSizeFull();
        this.setCompositionRoot(vlRoot);
    }

    private void setElementsId() {
        lblPageInfo.setId("lbl_nowpage");
        btnCurrentPage.setId("btn_currentpage");
        btnFirstPage.setId("btn_firstpage");
        btnLastPage.setId("btn_lastpage");
        btnPreviousPage.setId("btn_previouspage");
        btnNextPage.setId("btn_nextpage");
    }

    private void initGrid() {
        grid.addStyleNames(MaterialTheme.GRID_BORDERLESS);
        grid.addColumn(new ValueProvider<T, Integer>() {
            private static final long serialVersionUID = 2776458444454640094L;

            @Override
            public Integer apply(T source) {
                return data.indexOf(source) + startPosition + 1;
            }
        }).setCaption(I18NUtility.getValue("common.id", "ID")).setMinimumWidth(60);
        grid.addItemClickListener(event -> {
            if (objectClickListener != null) {
                objectClickListener.itemClicked(event);
            }
        });
        grid.addSelectionListener(new SelectionListener<T>() {
            private static final long serialVersionUID = 4404475737160537026L;

            @Override
            public void selectionChange(SelectionEvent<T> event) {
                if (objectSelectionListener != null) {
                    objectSelectionListener.itemClicked(event);
                }
            }
        });

        grid.addSortListener(listener -> {
            sortOrder = listener.getSortOrder();
            refreshCurrentPage();
        });
        grid.setSizeFull();
    }

    @Override
    public void setStartPage(int currentPage, int startPosition) {
        this.currentPage = currentPage;
        this.startPosition = startPosition;
    }

    @Override
    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }

    @Override
    public Column<T, ?> addColumn(String propertyName) {
        return grid.addColumn(propertyName, new TextRenderer()).setSortOrderProvider(direction -> {
            List<QuerySortOrder> list = new ArrayList<>();
            list.add(new QuerySortOrder(propertyName, direction));
            return list.stream();
        }).setHidable(true);
    }

    @Override
    public Column<T, ?> addColumn(String propertyName, AbstractRenderer<? super T, ?> renderer) {
        return grid.addColumn(propertyName, renderer).setSortOrderProvider(direction -> {
            List<QuerySortOrder> list = new ArrayList<>();
            list.add(new QuerySortOrder(propertyName, direction));
            return list.stream();
        }).setHidable(true);
    }

    @Override
    public <V> Column<T, V> addColumn(ValueProvider<T, V> valueProvider) {
        return grid.addColumn(valueProvider, new TextRenderer()).setHidable(true);
    }

    @Override
    public <V> Column<T, V> addColumn(ValueProvider<T, V> valueProvider,
        AbstractRenderer<? super T, ? super V> renderer) {
        return grid.addColumn(valueProvider, renderer).setHidable(true);
    }

    @Override
    public <V> Column<T, V> addColumn(ValueProvider<T, V> valueProvider,
        ValueProvider<V, String> presentationProvider) {
        return grid.addColumn(valueProvider, presentationProvider).setHidable(true);
    }

    @Override
    public <V, P> Column<T, V> addColumn(ValueProvider<T, V> valueProvider, ValueProvider<V, P> presentationProvider,
        AbstractRenderer<? super T, ? super P> renderer) {
        return grid.addColumn(valueProvider, presentationProvider, renderer).setHidable(true);
    }

    @Override
    public <V extends Component> Column<T, V> addComponentColumn(ValueProvider<T, V> componentProvider) {
        return grid.addComponentColumn(componentProvider).setHidable(true);
    }

    @Override
    public void setData(List<T> data) {
        this.data = data;
        this.isLocalModel = true;
        totalCount = data.size();
    }

    @Override
    public void refresh() {
        currentPage = 1;
        startPosition = 0;
        refreshCurrentPage();
    }

    /*
     * (non-Javadoc)
     *
     * @see com.ags.jaspermes.ui.component.IDomainObjectGrid#refresh()
     */
    @Override
    public void refreshCurrentPage() {
        if (isLocalModel) {
            ListDataProvider<T> ofCollection = DataProvider.ofCollection(data);
            grid.setDataProvider(ofCollection);
            if (data.isEmpty()) {
                totalCount = 0;
                currentPage = 1;
                updateButtonStatus();
                updatePageInfo();
            }

        } else {
            pagingQuery.init();
            PageModel<T> pageModel = pagingQuery.list(new PageInfo(currentPage, pageSize));

            data = pageModel.getRecords();
            totalCount = pageModel.getTotalCount();

            ListDataProvider<T> ofCollection = DataProvider.ofCollection(data);
            grid.setDataProvider(ofCollection);
            updateButtonStatus();
            updatePageInfo();

            // 选中外部需要选中的对象,选中后,从集合中删除
            Set<T> tempSelected = new HashSet<>();
            DataProvider<T, ?> dataProvider = grid.getDataProvider();
            Collection<?> items = ((ListDataProvider<?>)dataProvider).getItems();
            if (dataProvider instanceof ListDataProvider) {
                selectedPassedByOutside.forEach(object -> {
                    if (items.contains(object)) {
                        tempSelected.add(object);
                        grid.select(object);
                    }
                });
            }
            selectedPassedByOutside.removeAll(tempSelected);
        }
    }

    @Override
    public void clear() {
        grid.setDataProvider(DataProvider.ofCollection(Collections.emptyList()));
        totalCount = 0;
        currentPage = 1;
        startPosition = 0;
        selectedCache.clear();
        selectedPassedByOutside.clear();
    }

    @Override
    public void refresh(T item) {
        grid.getDataProvider().refreshItem(item);
    }

    @Override
    public void setObjectClickListener(IObjectClickListener<T> listener) {
        this.objectClickListener = listener;
    }

    @Override
    public void setObjectSelectionListener(IObjectSelectionListener<T> listener) {
        this.objectSelectionListener = listener;
    }

    @Override
    public T getSelectedObject() {
        Iterator<T> iterator = grid.getSelectedItems().iterator();
        if (iterator.hasNext()) {
            return iterator.next();
        } else {
            return null;
        }
    }

    @Override
    public void addStyleNameToGrid(String style) {
        grid.addStyleName(style);
    }

    @Override
    public Grid<T> getGird() {
        return grid;
    }

    @Override
    public SelectionMode getSelectionMode() {
        GridSelectionModel<T> selectionModel = grid.getSelectionModel();
        SelectionMode mode = null;
        if (selectionModel.getClass().equals(SingleSelectionModelImpl.class)) {
            mode = SelectionMode.SINGLE;
        } else if (selectionModel.getClass().equals(MultiSelectionModelImpl.class)) {
            mode = SelectionMode.MULTI;
        } else if (selectionModel.getClass().equals(NoSelectionModel.class)) {
            mode = SelectionMode.NONE;
        }
        return mode;
    }

    @Override
    public void setSelectionMode(SelectionMode selectionMode) {
        grid.setSelectionMode(selectionMode);
        grid.addSelectionListener(new SelectionListener<T>() {

            /**
             *
             */
            private static final long serialVersionUID = 1L;

            @Override
            public void selectionChange(SelectionEvent<T> event) {
                if (objectSelectionListener != null) {
                    objectSelectionListener.itemClicked(event);
                }
            }
        });
    }

    @Override
    public List<T> getSelections() {
        Set<T> selections = new HashSet<>();
        if (isMultipleSelected()) {
            selectedCache.put(currentPage, grid.getSelectedItems());
            selectedCache.values().forEach(tempSelectedSet -> {
                selections.addAll(tempSelectedSet);
            });
            selections.addAll(selectedPassedByOutside);
        } else {
            selections.addAll(grid.getSelectedItems());
        }
        return selections.stream().collect(Collectors.toList());
    }

    @Override
    public void select(List<T> selected) {
        selectedPassedByOutside.addAll(selected);
        grid.deselectAll();
        selected.forEach(grid::select);
    }

    @Override
    public void select(T selected) {
        selectedPassedByOutside.add(selected);
        grid.deselectAll();
        grid.select(selected);
    }

    /**
     * 不选中某个对象
     *
     * @param deselectItem
     */
    @Override
    public void deselect(T deselectItem) {
        grid.deselect(deselectItem);
    }

    /**
     * 不选中所有已经选中的对象
     */
    @Override
    public void deselectAll() {
        grid.deselectAll();
        selectedCache.clear();
        selectedPassedByOutside.clear();
    }

    @Override
    public void setSort(String columnName) {
        columnNameList.add(columnName);
    }

    @Override
    public void clearSortColumnName() {
        columnNameList.clear();
    }

    @Override
    public void setIsDialog(boolean isDialog) {
        this.isDialog = isDialog;
        selectedCache.clear();
    }

    @Override
    public void buttonClick(ClickEvent event) {
        Button button = event.getButton();
        button.setEnabled(true);
        putSelectedCache();
        if (button.equals(btnFirstPage)) {
            currentPage = 1;
            startPosition = 0;
        } else if (button.equals(btnLastPage)) {
            currentPage = totalCount % pageSize == 0 ? totalCount / pageSize : totalCount / pageSize + 1;
            startPosition = (currentPage - 1) * pageSize;
        } else if (button.equals(btnPreviousPage)) {
            if (currentPage - 1 != 0) {
                currentPage--;
            }
            startPosition = (currentPage - 1) * pageSize;
        } else if (button.equals(btnNextPage)) {
            if (currentPage * pageSize < totalCount) {
                currentPage++;
            }
            startPosition = (currentPage - 1) * pageSize;
        }
        updateButtonStatus();
        updatePageInfo();
        refreshCurrentPage();
        grid.scrollToStart();
        selectItemBySelectedCache();
    }

    private void selectItemBySelectedCache() {
        if (isMultipleSelected()) {
            Set<T> selected = selectedCache.get(currentPage);
            GridSelectionModel<T> selectionModel = grid.getSelectionModel();
            if (selected != null) {
                ((MultiSelectionModel<T>)selectionModel).updateSelection(new HashSet<>(selected), new HashSet<>());
            }
        }
    }

    private void updateButtonStatus() {
        if(totalCount >0){
            if (startPosition + pageSize < totalCount) {
                btnNextPage.setEnabled(true);
                btnLastPage.setEnabled(true);
            } else {
                btnNextPage.setEnabled(false);
                btnLastPage.setEnabled(false);
            }
            if (currentPage == 1) {
                btnFirstPage.setEnabled(false);
                btnPreviousPage.setEnabled(false);
            } else {
                btnFirstPage.setEnabled(true);
                btnPreviousPage.setEnabled(true);
            }
        } else{
            btnNextPage.setEnabled(false);
            btnLastPage.setEnabled(false);
            btnFirstPage.setEnabled(false);
            btnPreviousPage.setEnabled(false);
        }

    }

    private void updatePageInfo() {
        if (totalCount > 0) {
            int i = totalCount % pageSize;
            int page = totalCount / pageSize;
            int pageCount = page + (i == 0 && page != 0 ? 0 : 1);
            btnCurrentPage.setVisible(true);
            btnCurrentPage.setCaption(String.valueOf(currentPage));
            if (columnNameList.size() > 2) {
                lblPageInfo.setValue(I18NUtility.getValue("common.page.totalcount", "Total Count")
                    + I18NUtility.getValue("common.colon", ":") + totalCount + " | "
                    + I18NUtility.getValue("common.page.totalpage", "Total Page")
                    + I18NUtility.getValue("common.colon", ":") + pageCount);
            } else {
                lblPageInfo.setValue(I18NUtility.getValue("common.page.totalpage", "Total Page")
                    + I18NUtility.getValue("common.colon", ":") + pageCount);
            }
        } else {
            lblPageInfo.setValue("");
            btnCurrentPage.setVisible(false);
        }

    }

    @Override
    public void setFilter(IFilter filter) {
        pagingQuery.setFilter(filter);
        selectedCache.values().forEach(tempSelected -> {
            selectedPassedByOutside.addAll(tempSelected);
        });
        this.isLocalModel = false;
        selectedCache.clear();
    }

    private void putSelectedCache() {
        if (isMultipleSelected()) {
            selectedCache.put(currentPage, grid.getSelectedItems());
        }
    }

    private boolean isMultipleSelected() {
        return getSelectionMode().equals(SelectionMode.MULTI);
    }

    public void setLocalModel(boolean localModel) {
        isLocalModel = localModel;
    }

    @Override
    public int getCurrentPage() {
        return currentPage;
    }

    @Override
    public int getPageSize() {
        return pageSize;
    }

    @Override
    public int getTotalCount() {
        return totalCount;
    }

    @Override
    public int getStartPosition() {
        return startPosition;
    }

}

另外一种是针对于平台Domain对象分页的 PaginationDomainObjectList 分页组件,它继承自 PaginationObjectListGrid,实现的接口信息如下:

/**
 * 分页表格接口,用于指定的系统对象,主要用于单表分页的加载
 */
public interface IDomainObjectGrid<T extends ObjectBaseImpl<?>> extends IObjectListGrid<T> {

    /**
     * 自动加入固定的栏位: 名称、创建人,创建时间,最后修改人,最后修改时间。
     *
     * @param hasName
     *            是否需要显示名称栏位
     */
    void attachFixedColumns(boolean hasName);

    /*
     * 刷新当前页的数据
     */
    void refreshCurrentPage();

    /**
     * 需要加载对象的后台服务,是分页表格获取数据的通道。
     *
     * @param serviceClass
     *            后台服务的class
     * @param serviceBeanName
     *            后台服务的名称
     */
    void setServiceClass(Class<?> serviceClass, String serviceBeanName);

    /**
     * 需要加载对象的后台服务,是分页表格获取数据的通道。
     *
     * @param serviceClass
     *            后台服务的class
     */
    public void setServiceClass(Class<?> serviceClass);

    void setColumns(IObjectType type, SetColumnCallBack<T> callBack);


    void setColumns(IObjectType type);
}
另外,请根据实际需求选择是否调用方法attachFixedColumns(boolean hasName),此方法会根据代码执行顺序先后加入固定的栏位: 名称、创建人,创建时间,最后修改人,最后修改时间(其中参数hasName代表‘是否需要显示名称栏位’)。
并根据实际情况放置 attachFixedColumns 方法,一般放在所有特有属性的column添加的代码之后。
例子:工单数据分页显示
private IDomainObjectGrid<WorkOrder> gridOrder = new PaginationDomainObjectList<>();
gridOrder.setServiceClass(IWorkOrderService.class);//设置后台服务
gridOrder.attachFixedColumns(false);

页面具体效果如下图所示:

..\images\core\pagination

1.19.2. 系统配置项组件

系统目前定义的所有的配置项都是在页面初始化的时候调用 loadConfigurations() 方法, ConfigurationService 中的getByNameAndCategory(String name, String category)方法是根据配置项的名称和类别进行查询,如果该配置项不存在,那么将会按照所传入的名称和类别创建一个配置项,并且保存。

如果想要添加系统配置项,先定义两个常量,即Configuration的Category和Name。

系统已经定义的配置项组件有:

ComboBoxConfigurationItem: 复选框

PasswordConfigurationItem: 密码框

BooleanConfigurationItem: 单选框

TextConfigurationItem: 文本输入框

ConfigurationItemPanel: 每个配置项页面显示的Panel

ConfigurationItemDialog: 配置项的编辑弹窗

具体使用方法如下(以ConfigurationItemPanel为例):

    /**
    * 页面添加小数点精确位数的设置
    */
    private void addDigitConfigurationPanel() {
        //创建ConfigurationItemPanel对象
        ConfigurationItemPanel digitCfgPanel = new ConfigurationItemPanel();
        //定义caption
        digitCfgPanel.setCaption(I18NUtility.getValue("Admin.System.Setting.DigitSetting", "Exact Digit Configuration"));
        //ConfigurationItemPanel中添加
        TextConfigurationItem digit = new TextConfigurationItem(false)组件;
        digitCfgPanel.addConfigurationItem(digit);
        //系统配置页面添加ConfigurationItemPanel组件
        vlContent.addComponent(digitCfgPanel);

        digitCfgPanel.addItemSaveCallback(new ConfigurationItemSaveCallBack() {

            @Override
            public void done(ConfirmResult result) { }
        });
    }

1.19.3. 正则表达式生成组件

此组件为通用组件,用来生成正则表达式,目前支持五种类型的选择:

 //固定值
 CONSTANT("Constant", "Configuration.ExpressionType.Constant", ""),
 //数字
 NUMBER("Number", "Configuration.ExpressionType.Number", "[0-9]"),
 //字母
 CHARACTER("Character", "Configuration.ExpressionType.Character", "[a-zA-Z]"),
 //数字或字母
 NUMBER_AND_CHARACTER("NumberAndCharacter", "Configuration.ExpressionType.NumberAndCharacter", "[a-zA-Z0-9]"),
 //特殊字符
 SPECIAL_CHARACTER("SpecialCharacter", "Configuration.ExpressionType.SpecialCharacter", "[!@#$%^&*()_+=\\-\\s]");

目前在MES项目中 AddPartDialog 中有使用到此组件,代码示例如下:

Binder<Part> binder = new Binder<>();

//定义一个正则表达式生成组件
private MatcherExpressionEditor meValidateRules = new MatcherExpressionEditor();

//将组件加入到页面中
HorizontalLayout hl = new HorizontalLayout();
hl.addComponent(meValidateRules);

//此组件继承自CustomField<String>,所以可以在binder中使用
binder.forField(meValidateRules).bind(Part::getValidateRules, Part::setValidateRules);

此组件中定义了一个文本输入框和一个编辑按钮,点击编辑按钮,会出现下图中右侧的编辑表达式的弹窗

rule validate

校验规则的使用

例如在新建物料时设置了序列号校验,在操作中心物料过站时,扫描序列号时需要验证序列号是否符合校验规则,具体代码调用示例如下:

public class MatcherExpressionItem {

    private MatcherExpressionType itemType;
    private String itemValue;
    private int no;
    private MatcherExpressionTypeChangeListener typeChangeListener;

    public MatcherExpressionItem() {}

    public MatcherExpressionItem(int no) {
        this.no = no + 1;
    }

    public MatcherExpressionItem(int no, MatcherExpressionType type, String itemValue) {
        this.no = no;
        this.itemType = type;
        this.itemValue = itemValue;
    }

    public static List<MatcherExpressionItem> parseExpression(String persistenceExpression) {
        List<MatcherExpressionItem> items = new ArrayList<>();
        if (!Strings.isNullOrEmpty(persistenceExpression)) {
            String[] arrItems = persistenceExpression.split("\\" + CommonConstants.EXPRESSION_CONNECTOR);
            MatcherExpressionItem itemTemp = null;
            for (String arrItem : arrItems) {
                String valueOrLen = "";
                String typeTemp = arrItem;
                if (arrItem.indexOf('(') >= 0) {
                    arrItem = arrItem.replace(")", "");
                    String[] split = arrItem.split("\\(");
                    typeTemp = split[0];
                    valueOrLen = split[1];
                }
                MatcherExpressionType type = MatcherExpressionType.getValue(typeTemp);
                itemTemp = new MatcherExpressionItem(items.size());
                itemTemp.setItemType(type);
                itemTemp.setItemValue(valueOrLen);
                items.add(itemTemp);
            }
        }
        return items;
    }

    public static String getDisplay(List<MatcherExpressionItem> items) {
        StringBuilder res = new StringBuilder();
        items.sort((item1, item2) -> item1.getNo() - item2.getNo());
        for (MatcherExpressionItem item : items) {
            res.append(I18NUtility.getValue(item.getItemType().getNameKey(), item.getItemType().getName()));
            res.append("(" + item.getItemValue() + ")");
            res.append(CommonConstants.EXPRESSION_CONNECTOR);
        }
        String strRes = res.toString();
        if (strRes.endsWith(CommonConstants.EXPRESSION_CONNECTOR)) {
            strRes = strRes.substring(0, strRes.lastIndexOf(CommonConstants.EXPRESSION_CONNECTOR));
        }
        return strRes;
    }

    public static String toMatcherExpression(String persistenceExpression) {
        return toMatcherExpression(parseExpression(persistenceExpression));
    }

    public static String toMatcherExpression(List<MatcherExpressionItem> items) {
        StringBuilder res = new StringBuilder();
        for (int index = 0; index < items.size(); index++) {
            MatcherExpressionItem item = items.get(index);
            if (index == 0) {
                res.append('^');
            }
            if (MatcherExpressionType.CONSTANT.equals(item.getItemType())) {
                res.append(item.getItemValue());
            } else {
                res.append(item.getItemType().getMatcherExpression());
                res.append('{');
                res.append(item.getItemValue());
                res.append('}');
            }
            if (index == (items.size() - 1)) {
                res.append('$');
            }
        }
        return res.toString();
    }

    public static String toPersistenceExpression(List<MatcherExpressionItem> items) {
        StringBuilder res = new StringBuilder();
        items.sort((item1, item2) -> item1.getNo() - item2.getNo());
        for (MatcherExpressionItem item : items) {
            res.append(item.getItemType().getName());
            res.append("(" + item.getItemValue() + ")");
            res.append(CommonConstants.EXPRESSION_CONNECTOR);
        }
        String strRes = res.toString();
        if (strRes.endsWith(CommonConstants.EXPRESSION_CONNECTOR)) {
            strRes = strRes.substring(0, strRes.lastIndexOf(CommonConstants.EXPRESSION_CONNECTOR));
        }
        return strRes;
    }

    public MatcherExpressionType getItemType() {
        return itemType;
    }

    public void setItemType(MatcherExpressionType itemType) {
        this.itemType = itemType;
        if (Objects.nonNull(typeChangeListener)) {
            typeChangeListener.typeChange(itemType);
        }
    }

    public String getItemValue() {
        return itemValue;
    }

    public void setItemValue(String itemValue) {
        this.itemValue = itemValue;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public MatcherExpressionTypeChangeListener getTypeChangeListener() {
        return typeChangeListener;
    }

    public void setTypeChangeListener(MatcherExpressionTypeChangeListener typeChangeListener) {
        this.typeChangeListener = typeChangeListener;
    }

    public interface MatcherExpressionTypeChangeListener {
        void typeChange(MatcherExpressionType type);
    }

}
其中toMatcherExpression方法返回一个正则表达式,然后调用Pattern的matches方法判断是否匹配。

1.19.4. 对象选择框

对象选择框供用户选择(一个/多个)对象去执行某些逻辑,以 Window 的形式展现,将所有对象列出以供用户选择,选择框里会显示对象的名称和描述,其数据来源于对象的后台服务接口。

效果图如下(以系统中故障代码选择处理代码组为例):

failurecode

具体使用方法参考如下代码(摘自FailureCodeViewImpl.java)

     @Inject
     private IObjectSelectDialog objectSelectDialog;

     else if (btnModifyActionGroup.equals(button)) {
         //设置后台服务的名称,此为表格获取数据的通道
         objectSelectDialog.setService(IFailureActionGroupService.class);
         objectSelectDialog.setSort(FailureActionCodeGroupEntity.NAME);
         objectSelectDialog.setIsDialog(true);
         //是否必选
         objectSelectDialog.setMustSelect(false);
         //设置选择框的caption
         objectSelectDialog.setCaption(I18NUtility.getValue("Common.Select", "Select") + " "
             + I18NUtility.getValue("Configuration.Failure.FailureActionCodeGroup", "Failure Action Code Group"));
         List<BuildtimeObjectBaseImpl<?>> fcs = new ArrayList<>();
          //设置已经被绑定的数据页面展示是选中状态
         objectSelectDialog.select(fcs);
         objectSelectDialog.show(getUI(), new DialogCallBack() {
             @Override
             public void done(ConfirmResult result) {

             }
         });
     }
对象选择框支持设置选中模式、设置排序、设置过滤器等方法,具体使用方法请查看Api。
如需加入自定义的方法,可继承IObjectSelectDialog接口进行添加。
IObjectSelectDialog接口定义的代码如下:
public interface IObjectSelectDialog<T extends ObjectBaseImpl<?>> extends IBaseDialog {
    void setService(Class<?> serviceClass);

    void setCaption(String caption);

    void setSelectionMode(SelectionMode selectionMode);

    void select(List<T> selected);

    void selectOne(T selected);

    List<T> getSelections();

    void setData(List<T> datas);

    void setIsDialog(boolean isDialog);

    void setMustSelect(boolean mustSelect);

    void setSort(String columnName);

    void setEntityFilter(EntityFilter entityFilter);
}

1.19.5. 二维码/条形码组件

系统提供了用于生成二维码/条形码的Vaadin组件,也有相应的restful接口来生成二维码/条形码。

  • QRCodeImage(二维码组件) & BarCodeImage(条形码组件)
    例如,系统管理项中的条形码即由 BarCodeImage 组件生成,效果图如下:

barcode

如何使用,参考如下代码:

ConfigurationItemPanel codeCfgPanel = new ConfigurationItemPanel();
private BarCodeImage barcode = new BarCodeImage(); //定义一个条形码组件
barcode.setWidth("100px");
barcode.setHeight("60px");
codeCfgPanel.addComponent(barcode); //将组件加入到页面中
qrCode.setValue("......."); //为条形码组件传入具体的值
  • 系统还提供了 QRCodeUtils(二维码工具类)和 BarCodeUtils(条形码工具类), 可根据用户的输入生成相应的二维码/条形码。
    例如,系统首页中的二维码即由 QRCodeUtils 工具类生成,效果图如下:

two dimensional code

如何使用,参考如下代码:

Map<String, String> valueMap = new HashMap<>();
valueMap.put("aa","dddd");
Object valuMapJson = JSONObject.toJSON(valueMap);
byte[] byteArray = QRCodeUtils.generateQRCode(valuMapJson.toString());
String byteString = Base64Utils.encodeToString(byteArray);
return new RestResponse<>(RestResponseCode.OK, true, "success", byteString);
  • 生成二维码/条形码的 restful 接口

详细说明请参考系统中有关生成二维码/条形码的 restful API 文档。

1.19.6. 扩展属性编辑组件

系统对所支持的扩展属性已做了特殊处理,即在对象编辑的dialog中会自动显示扩展属性,会跟固有属性一样正常编辑和保存,但如有特殊情况仍需单独对扩展属性进行处理,此组件将会满足需求。

系统中只有极个别页面使用了此组件

例如, Permission 页面中使用了此组件来查看其扩展属性,效果图如下:

customized edit dialog

代码示例如下:(摘自PermissionView.java)

    // 扩展属性按钮
    private Button btnCF = new Button();

    //扩展属性编辑组件
    @Inject
    private IEditCustomizedFieldDialog cfDialog;

    if (clickedButton.equals(btnCF)) {
        PermissionWrapper selectPermission = (PermissionWrapper)this.objectGrid.getSelectedObject();
            cfEditDialog.setObjectExtension(selectPermission.getPermission());
            cfEditDialog.show(getUI(), null);
    }

1.19.7. RoundImage组件

页面的展示效果如下

..\images\core\round image

代码示例如下:

    private RoundImage image = new RoundImage();
    //将组件加入到页面中
    HorizontalLayout hl = new HorizontalLayout();
    hl.addComponent(image);

1.19.8. ChartJS 组件

平台本身提供了chart js的支持,包括饼图、柱状图、折线图等等。

详细请参阅官方demo:
Vaadin ChartJS 官方文档

UI中添加chart

以添加一个BarChart(柱状图)为例,代码示例如下:

为了更好的匹配我们的浅色和深色主题,平台在 ChartThemeUtils 工具类中提供了chart的样式渲染,调用方式详见代码中的注释。
BarChartConfig barConfig = new BarChartConfig();
barConfig.data().labels("January", "February", "March", "April", "May", "June", "July")
    .addDataset(new BarDataset())
        .label("Dataset 1").yAxisID("y-axis-1"))
    .addDataset(new BarDataset())
        .label("Dataset 2").yAxisID("y-axis-2").hidden(true))
    .and();

//为chart做颜色等样式的渲染 ,此方法需在创建ChartJs之前调用
ChartThemeUtils.optimizeStyle(config);

//创建chart组件
ChartJs chart = new ChartJs(config);
chart.setJsLoggingEnabled(true);

//将chart组件加入到页面中
HorizontalLayout hlBarChart = new HorizontalLayout();
hlBarChart.addComponent(chart);

效果图如下:

ChartJs

1.19.9. 查询条件组件

平台提供了自定义的查询组件(SearchPanelBuilder),用于支持对象的多条件查询,风格统一而且便于维护。

效果图如下:

SearchPanel
首先,添加一个Conditions

Conditions即查询条件,实现自 IConditions 接口,代码如下:

public interface IConditions {

    IFilter getFilter();

    void reset();

    /**
     * 返回查询条件的控件信息。 这个方法返回直接显示在搜索区域的所有控件(不建议 放多于1个 会影响布局),将直接显示在搜索区域,对于其他的条件,调用下面的方法返回其余的控件 以及布局。
     *
     * @return
     */
    Component[] getComponent();

    /**
     * 返回点击更多下的布局信息
     *
     * @return
     */
    AbstractLayout getLayout();
}

operation 对象为例, OperationConditions 的代码如下:

其中一些注意事项参考代码中的注释部分。

@SpringComponent
@Scope("prototype")
@SearchConfiguration("Operation")
public class OperationConditions extends BaseConditions implements IConditions {

    FormLayout hlRoot = new FormLayout();

    @GridElement(value = "Name")
    private TextField tfOperationName = new TextField();

    @GridElement(value = "Category")
    @I18Support(caption = "Category", captionKey = "common.category")
    private ComboBox<OperationType> cbCategory = new ComboBox<>();

    //components中的组件将被添加至search bar中。
    private Component[] components = {tfOperationName};

    private List<HasValue<?>> fields;

    private ConditionComponentHelper conditionComponent = new ConditionComponentHelper(MesObjectType.Operation);

    public OperationConditions() {

        //显示在search bar中的控件不能有caption,请用Placeholder来代替。
        tfOperationName.setPlaceholder(I18NUtility.getValue("Common.Name", "Name"));
        conditionComponent.initComponent();
        tfOperationName.addStyleName(MaterialTheme.TEXTFIELD_BORDERLESS);

        //allComponent中的组件将被添加至Condition中,显示在弹出框中。
        List<Component> allComponent = new ArrayList<>();
        allComponent.add(cbCategory);
        allComponent.addAll(conditionComponent.getExtendedComponents());
        fields = conditionComponent.getExtendedComponentValues();

        //根容器请不要设置宽度100%,否则弹出框的宽度会与屏幕一样宽,即铺满整个屏幕。
        hlRoot.setWidthUndefined();
        for (Component component : allComponent) {

            //同样,弹出框中的组件宽度也需要设置为Undefined。
            component.setWidthUndefined();
            hlRoot.addComponent(component);
        }
        cbCategory.setDataProvider(DataProvider.ofItems(OperationType.values()));
        cbCategory.setItemCaptionGenerator(item -> I18NUtility.getValue(item.getNameKey(), item.name()));
        setElementsId();
    }

    private void setElementsId() {
        tfOperationName.setId("tf_name");
        cbCategory.setId("cb_category");
    }

    @Override
    public IFilter getFilter() {
        IOperationService operationService = BeanManager.getService(IOperationService.class);

        EntityFilter createFilter = operationService.createFilter();
        if (!StringUtils.isEmpty(tfOperationName.getValue())) {
            createFilter.fieldContains(OperationEntity.NAME, tfOperationName.getValue());
        }
        if (cbCategory.getValue() != null) {
            createFilter.fieldContains(OperationEntity.CATEGORY, cbCategory.getValue().getName());
        }
        conditionComponent.getFilter(createFilter);
        return createFilter;
    }

    @Override
    public BasePresenter<?> getPresenter() {
        return null;
    }

    @Override
    public void reset() {
        tfOperationName.clear();
        cbCategory.clear();
        for (HasValue<?> field : fields) {
            field.clear();
        }
    }

    @Override
    public Component[] getComponent() {
        return components;
    }

    @Override
    public AbstractLayout getLayout() {
        return hlRoot;
    }
}
然后,创建SearchPanelBuilder,并将其加入到页面中

以Operation页面中SearchPanelBuilder为例,代码示例如下:

//创建一个SearchPanel,请注意Conditions的引入方式
SearchPanelBuilder searchPanel = new SearchPanelBuilder(BeanManager.getService(OperationConditions.class), objectGrid, this);

//将searchPanel加入到页面中
hlToolBox.addComponent(searchPanel);

//将searchPanel在容器中靠右对齐
hlToolBox.setComponentAlignment(searchPanel, Alignment.MIDDLE_RIGHT);
如果没有更多条件(即Conditions为空),系统会默认隐藏【更多】按钮。

1.19.10. 图片筛选组件

该组件与RoundImage组件相似,RoundImage组件一般是放在添加dialog中上传一张图片的显示组件,而该组件可以显示多张图片,用户可对其选择的图片进行筛选,具体效果如下:

picture edit

如何使用该组件

    @Inject
    private IPictureEdit pictureEdit;

    //编辑按钮点击事件
   if (button.equals(btnEdit)) {
        Iterator<Component> iterator = addRow.iterator();
        List<Image> images = new ArrayList<>();
        while (iterator.hasNext()) {
            images.add(new Image(...));
        }
        pictureEdit.setPictures(images);
        pictureEdit.show(this.getUI(), null);
   }

1.19.11. 图片预览组件

该组件用于全屏查看上传的图片,效果如下:

image preview

代码示例如下:

    @Inject
    private IPicturePreview picturePreview;

    //预览按钮点击事件
    if (button.equals(btnPreview)) {
        List<ObjectStationImage> images = ...;
        InputStream[] inputStreams = new InputStream[images.size()];
        for (int i = 0; i < inputStreams.length; i++) {
            InputStream stream = images.get(i).getMedia().getMediaStream();
        }
        picturePreview.setInputStreams(inputStreams);
        picturePreview.show(getUI());
    }

1.19.12. 时间选择组件

系统提供了时间选择组件(TimeSelector),效果图如下:

timeselector

具体使用方法,代码示例如下:

//创建一个时间选择组件
TimeSelector tsStartTime = new TimeSelector();
//将组件加入到页面中
HorizontalLayout hl = new HorizontalLayout();
hl.addComponent(tsStartTime);

1.19.13. 文件上传组件

上传组件允许用户将文件上传至服务器,上传组件将接收到的数据写入到java.io.OutputStream中,因此您可以对上传的内容自由处理。

FileUploader组件对文件接收器及上传结束事件做了封装,结合UploadButton一起使用。

代码示例如下:

UploadButton uploadButton = new UploadButton();

FileUploader fileUploader = new FileUploader(event -> {
    event.getUploadFile();  //获取文件流InputStream
    event.getFileName();    //获取文件名
    ....
});

uploadButton.setReceiver(fileUploader); //文件接收器,由FileUploader提供,接收InputStream,返回OutputStream

uploadButton.addSucceededListener(fileUploader);    //文件上传成功监听器,在此FileUploader中处理

uploadButton.setImmediateMode(true);//在即时模式下,上载会显示一个文件名输入框和一个用于选择文件的按钮,选择文件后,即会立即开始上传。

1.19.14. 扫描组件

平台提供了两个用于扫描条形码/二维码的控件,一个是用于扫描的按钮,一个是带有输入框的扫描按钮,效果分别如下图所示:

此组件主要用于移动端,PC端无法使用。
scan preview

代码示例如下:

    // 组件1 ScannerButton
    ScannerButton btn = new ScannerButton();
    btn.setScanListener(new ScanListener() {

        @Override
        public void scan(String result) {
            System.out.println(result); // result为扫描到的结果
        }
    });

    // 组件2 ScannerField
    ScannerField filed = new ScannerField(); // 扫描到的结果会自动显示在该控件的输入框中
    filed.setWidth("300px");// 设置控件大小

    HorizontalLayout hlLayout = new HorizontalLayout();
    hlLayout.addComponent(btn,filed);   // 将控件加入到页面容器中

1.20. 系统时间处理

数据库获取的数据中时间字段的处理

当用户使用的时区和数据库服务器的时区不一致时,需要将数据库服务器的时区转换为用户使用的时区,如何转换,可参考如下代码:

RequestInfo 中提供了获取时区的方法,可直接使用。
    //公共属性
    gridOrder.addColumn(new ValueProvider<WorkOrder, String>() {
        private static final long serialVersionUID = 1L;

        @Override
        public String apply(WorkOrder source) {
            //不需要时区转换,直接格式化日期即可。
            return source.getCreateTime() != null
                ? source.getCreateTime().format(DateTimeFormatter.ofPattern(DateTimeUtils.DEFAULT_DATETIME))
                : null; (1)
        }
    });

   //非公共属性
    gridOrder.addColumn(new ValueProvider<WorkOrder, String>() {
        private static final long serialVersionUID = 1L;

        @Override
        public String apply(WorkOrder source) {
            return source.getClosedTime() != null
                ? source.getClosedTime().withZoneSameInstant(RequestInfo.current().getUserZoneId()) (2)
                    .format(DateTimeFormatter.ofPattern(DateTimeUtils.DEFAULT_DATETIME))
                : null;
        }
    });
1 公共属性,默认已经将时区设置为了用户对应的时区。
2 非公共属性,需要先转换成用户对应的时区。

如果采用sql语句查询出来的数据,在传到前端时一定要将时间有关的数据做处理,否则页面显示就不正确

    Timestamp start = (Timestamp)temp[1];
    wrapper.setStartTime(start == null ? "" :DateTimeUtils.format(start,RequestInfo.current().getUserZoneId()));
数据库服务器的时区要和应用服务器的时区一致
DateTimeUtils中提供了三种日期格式和多种时间转换的方法,根据实际情况使用
DateTimeUtils源代码
public class DateTimeUtils {

    public static final String DEFAULT_DATETIME = "yyyy-MM-dd HH:mm:ss";

    public final static String FormatterDefineOracle = "yyyy-mm-dd hh24:mi:ss";

    public final static String DEFAULT_DATE = "yyyy-MM-dd";

    /**
     * 格式化ZonedDateTime 默认格式yyyy-MM-dd HH:mm:ss 默认时区
     *
     * @param zonedDateTime
     * @return
     */
    public static String format(ZonedDateTime zonedDateTime) {
        return format(zonedDateTime, ZoneId.systemDefault(), DateTimeFormatter.ofPattern(DEFAULT_DATETIME));
    }

    /**
     * 根据指定时区格式化ZonedDateTime 默认格式yyyy-MM-dd HH:mm:ss
     *
     * @param zonedDateTime
     * @param zoneId
     * @return
     */
    public static String format(ZonedDateTime zonedDateTime, ZoneId zoneId) {
        return format(zonedDateTime, zoneId, DateTimeFormatter.ofPattern(DEFAULT_DATETIME));
    }

    /**
     * 根据指定的格式,格式化ZonedDateTime 默认时区
     *
     * @param zonedDateTime
     * @param format
     * @return
     */
    public static String format(ZonedDateTime zonedDateTime, String format) {
        return format(zonedDateTime, ZoneId.systemDefault(), DateTimeFormatter.ofPattern(format));
    }

    /**
     * 根据指定的格式和时区,格式化ZonedDateTime
     *
     * @param zonedDateTime
     * @param zoneId
     * @param format
     * @return
     */
    public static String format(ZonedDateTime zonedDateTime, ZoneId zoneId, String format) {
        return format(zonedDateTime, zoneId, DateTimeFormatter.ofPattern(format));
    }

    /**
     * 根据时区格式化日期Date
     *
     * @param date
     * @param zoneId
     * @return
     */
    public static String format(Date date, ZoneId zoneId) {
        return date.toInstant().atZone(zoneId).format(DateTimeFormatter.ofPattern(DEFAULT_DATETIME));
    }

    /**
     * 根据时区和格式,格式化日期zonedDateTime
     *
     * @param zonedDateTime
     * @param zoneId
     * @param dateTimeFormatter
     * @return
     */
    public static String format(ZonedDateTime zonedDateTime, ZoneId zoneId, DateTimeFormatter dateTimeFormatter) {
        return Objects.isNull(zonedDateTime) ? "" : zonedDateTime.withZoneSameInstant(zoneId).format(dateTimeFormatter);
    }

    /**
     * LocalDate转Date, 默认时区
     *
     * @param localDate
     * @return
     */
    public static Date getDate(LocalDate localDate) {
        return getDate(localDate, ZoneId.systemDefault());
    }

    /**
     * LocalDate转Date, 指定时区
     *
     * @param localDate
     * @param zoneId
     * @return
     */
    public static Date getDate(LocalDate localDate, ZoneId zoneId) {
        return Objects.isNull(localDate) ? null : Date.from(localDate.atStartOfDay().atZone(zoneId).toInstant());
    }

    /**
     * LocalDateTime转Date, 默认时区
     *
     * @param localDateTime
     * @return
     */
    public static Date getDate(LocalDateTime localDateTime) {
        return getDate(localDateTime, ZoneId.systemDefault());
    }

    /**
     * LocalDateTime转Date, 指定时区
     *
     * @param localDateTime
     * @param zoneId
     * @return
     */
    public static Date getDate(LocalDateTime localDateTime, ZoneId zoneId) {
        return Objects.isNull(localDateTime) ? null : Date.from(localDateTime.atZone(zoneId).toInstant());
    }

    /**
     * ZonedDateTime转Date
     *
     * @param zonedDateTime
     * @return
     */
    public static Date getDate(ZonedDateTime zonedDateTime) {
        return Objects.isNull(zonedDateTime) ? null : Date.from(zonedDateTime.toInstant());
    }

    /**
     * Date转LocalDate, 默认时区
     *
     * @param date
     * @return
     */
    public static LocalDate getLocalDate(Date date) {
        return getLocalDate(date, ZoneId.systemDefault());
    }

    /**
     * Date转LocalDate, 指定时区
     *
     * @param date
     * @param zoneId
     * @return
     */
    public static LocalDate getLocalDate(Date date, ZoneId zoneId) {
        return Objects.isNull(date) ? null : date.toInstant().atZone(zoneId).toLocalDate();
    }

    /**
     * LocalDateTime转LocalDate
     *
     * @param localDateTime
     * @return
     */
    public static LocalDate getLocalDate(LocalDateTime localDateTime) {
        return Objects.isNull(localDateTime) ? null : localDateTime.toLocalDate();
    }

    /**
     * Date转LocalDateTime, 默认时区
     *
     * @param date
     * @return
     */
    public static LocalDateTime getLocalDateTime(Date date) {
        return getLocalDateTime(date, ZoneId.systemDefault());
    }

    /**
     * Date转LocalDateTime, 指定时区
     *
     * @param date
     * @param zoneId
     * @return
     */
    public static LocalDateTime getLocalDateTime(Date date, ZoneId zoneId) {
        return Objects.isNull(date) ? null : LocalDateTime.ofInstant(date.toInstant(), zoneId);
    }

    /**
     * LocalDate转LocalDateTime
     *
     * @param localDate
     * @return
     */
    public static LocalDateTime getLocalDateTime(LocalDate localDate) {
        return Objects.isNull(localDate) ? null : localDate.atStartOfDay();
    }

    /**
     * Date转ZonedDateTime,默认时区
     *
     * @param date
     * @return
     */
    public static ZonedDateTime getZonedDateTime(Date date) {
        return getZonedDateTime(date, ZoneId.systemDefault());
    }

    /**
     * Date转ZonedDateTime,指定时区
     *
     * @param date
     * @param zoneId
     * @return
     */
    public static ZonedDateTime getZonedDateTime(Date date, ZoneId zoneId) {
        return Objects.isNull(date) ? null : ZonedDateTime.ofInstant(date.toInstant(), zoneId);
    }

    /**
     * LocalDateTime转ZonedDateTime,默认时区
     *
     * @param localDateTime
     * @return
     */
    public static ZonedDateTime getZonedDateTime(LocalDateTime localDateTime) {
        return Objects.isNull(localDateTime) ? null : getZonedDateTime(localDateTime, ZoneId.systemDefault());
    }

    /**
     * LocalDateTime转ZonedDateTime,指定时区
     *
     * @param localDateTime
     * @param zoneId
     * @return
     */
    public static ZonedDateTime getZonedDateTime(LocalDateTime localDateTime, ZoneId zoneId) {
        return Objects.isNull(localDateTime) ? null : localDateTime.atZone(zoneId);
    }

    /**
     * LocalDate转ZonedDateTime,默认时区
     *
     * @param localDate
     * @return
     */
    public static ZonedDateTime getZonedDateTime(LocalDate localDate) {
        return Objects.isNull(localDate) ? null : getZonedDateTime(localDate, ZoneId.systemDefault());
    }

    /**
     * LocalDate转ZonedDateTime,指定时区
     *
     * @param localDate
     * @param zoneId
     * @return
     */
    public static ZonedDateTime getZonedDateTime(LocalDate localDate, ZoneId zoneId) {
        return Objects.isNull(localDate) ? null : localDate.atStartOfDay(zoneId);
    }

    /**
     * 按指定格式解析时间字符串
     *
     * @param text
     * @param formatter
     * @return
     */
    public static ZonedDateTime parse(CharSequence text, String formatter) {
        return ZonedDateTime.parse(text, DateTimeFormatter.ofPattern(formatter));
    }

    /**
     * 按指定格式,指定时区解析时间字符串
     *
     * @param text
     * @param formatter
     * @param zoneId
     * @return
     */
    public static ZonedDateTime parse(CharSequence text, String formatter, ZoneId zoneId) {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(formatter).withZone(zoneId);
        return ZonedDateTime.parse(text, dateTimeFormatter);
    }

}

1.21. vaadin themes

1.21.1. 概述

自定义主题文件位于Web应用程序的 VAADIN/themes

主题目录结构图如下图所示(图片来自官网):

theme contents
valo为vaddin官方默认的内置主题,内置主题存储在 vaadin-themes.jar中

1.21.2. Material Theme

lumos-material 项目是一个独立的项目,它提供了一套基础样式库。您可以根据自己的设计随意修改里面的scss文件。

简介

Material主题位于 lumos-material 项目的 VAADIN/themes/ 文件夹下。

Material主题是Google Material Design规范的实现。我们在此基础上修改了一些组件的样式,并添加了一些自定义的样式。

默认情况下,所有Vaadin组件都实现了valo主题,所以Material主题也是以Valo为基本主题。

目录结构
material theme
主题文件

styles.scss文件中包含material主题文件(即material.scss),如下所示:

@import "material.scss";
.material {
    @include material;
}

material.scss文件中包含了所有组件及自定义等样式的引用,参数的定义(variables.scss)需要在@import语句之前引用。如下所示:

material scss
参数定义
valo中内置参数

一些常规的定义:

$v-focus-color: #2196F3 !default; --重点色
$v-background-color:#fafafa; --Valo主题的主要控制参数,它用于计算主题中的所有其他颜色
$v-app-background-color:#fafafa --UI的根元素的背景颜色。
$v-app-loading-text:“Loading Resources…​” --在加载和启动客户端引擎时,加载时显示的静态文本。
$v-line-height:1.6; --组件的高度,它以小数为单位制定。
$v-font-size:14px; --基本字体大小,它以像素为单位指定。
$v-font-color:14px; --基本字体大小,它以像素为单位指定。
$v-font-family: "Source Sans Pro", sans-serif !default; --基本字体。
$v-unit-size: 32px !default; --这是各种布局单元的基本大小,它直接用于某些控件的大小,例如按钮高度和Layout的边距。

有关特定于组件的样式的完整最新列表,请参阅ValoTheme中的Vaadin API文档。 Valo Theme 官方文档

Lumos中自定义参数

$jasper-background-color: #ecf0f5 !default; --主要用于view的背景色
$jasper-header-color: #273644 !default; --header.scss中使用,用于定义header的背景色
$jasper-header-height:50px !default; --在header.scss中使用,用于定义header的高度
$jasper-border-color: rgba(0,0,0,0.1); --边框的颜色,主要用于button及input等组件
$jasper-icon-size:12px; --图标的大小

常量定义

与valo内置主题相关的各种常量在 com.vaadin.ui.themes 包中的 ValoTheme 类中定义,这些主要是特定组件的特殊样式名称。

与material主题相关的各种常量在 com.github.appreciated.material 包中的 MaterTheme 类中定义。

与lumos主题相关的各种常量在 com.ags.lumosframework.web.vaadin.base 包中的 CoreTheme 类中定义。

//定义一个按钮,并为按钮添加 “只有图标”和 “圆角”和“背景为黄色”的样式
Button btn = new Button ("Button");
btn.addStyleNames(ValoTheme.BUTTON_ICON_ONLY, MaterialTheme.BUTTON_ROUND, CoreTheme.BACKGROUND_YELLOW);

1.21.3. 创建和使用自定义主题

自定义主题会被放置在Web应用程序的 VAADIN/themes 中,如果是maven项目,将位于 src/main/webapp 下,如果是Eclipse项目,将位于 WebContent 下,并且这个位置是固定的。另外,每创建一个主题都需要有一个主题文件夹。

1、主题需要在基础主题上进行扩展,不建议直接复制和修改基础主题。
2、主题文件夹的名称决定了主题的名称,名称用于@Theme注解。
3、一个自定义主题必须包含一个styles.scss文件,其他文件可以自由命名。
4、建议至少在两个SCSS文件中组织主题。
5、不需要手动添加 styles.css 文件,当浏览器请求theme时,此文件会在scss文件编译时自动生成。
创建自定义主题

以创建一个名为 mytheme 的主题为例:

1、在 VAADIN/themes 下创建一个名为 mytheme 的文件夹。
2、在 mytheme 文件夹下创建一个 styles.scss 文件来定义主题的规则。
styles.scss 中,建议将定义的规则以主题名称为选择器,如:将规则都包含在 .mytheme

@import "addons.scss";
@import "mytheme.scss";

.mytheme {
  @include addons;
  @include mytheme;
}

3、在 mytheme 文件夹下创建一个 mytheme.scss 作为实际的主题文件。实际主题定义如下:

实际主题作为mixin,并包含了基本主题material。

@import "../material/material.scss";

@mixin mytheme {
  @include material;

  /* An actual theme rule */
  .v-button {
    color: blue;
  }
}
建议主题文件使用实际的主题名来作为前缀,例如:mytheme.scss,dark.scss。
add-on theme

如果使用了包含scss的vaadin插件,当引入这些插件时,Eclipse的Vaadin插件或Maven会将其对应的规则自动添加到addons.scss文件中,并被包含在styles.scss中。该addons.scss文件由Eclipse的Vaadin插件或Maven自动生成,不需要手动添加。

主题应用

可以通过为应用程序的UI类使用@Theme注解来指定主题,如下所示:

@Theme("mytheme")
public class MyUI extends UI {
    @Override
    protected void init(VaadinRequest request) {
        ...
    }
}
样式标准组件

Vaadin中的每个组件都有一个CSS样式类,您可以使用它来控制组件的样式。可以使用addStyleName()添加定义好的样式名。

Button smallButton = new Button ("Small Valo Button");
smallButton.addStyleName(ValoTheme.BUTTON_SMALL);
getStyleName() 仅返回自定义样式名,而不返回组件的内置样式名。

有关其样式的说明,请参阅每个组件的介绍。

Lumos主题

Lumos提供了两种自定义主题:light(浅色)和dark(深色),主题文件位于 lumos-vaadin 项目的 VAADIN/themes/ 文件夹下。

light

此主题基于material主题,基本没做修改。

dark

此主题基于material主题,对背景色等参数做了修改,详见dark主题下的 variables.scss

如何切换Theme

如何将自定义主题应用到整个应用程序或部分应用,以dark主题为例:

1.永久使用dark主题

在UI中使用 @Theme 注解,这是最简单也是最推荐的方法。

@Theme("dark")
public class MainUI extends UI {
  //...
}

2.在light和dark中自由切换

在页面上提供一个按钮或者单选按钮,来让用户决定使用哪种主题。如:

public class MainUI extends UI {

  public MainUI() {
    RadioButtonGroup<VaadinTheme> themeRbg = new RadioButtonGroup<VaadinTheme>();
    themeRbg.addValueChangeListener(new ValueChangeListener<VaadinTheme>() {
            @Override
            public void valueChange(ValueChangeEvent<VaadinTheme> event) {
                String selected = event.getValue().getName();
                UI currentUI = UI.getCurrent();
                if (currentUI != null) {
                    currentUI.setTheme(selected);
                }
            }
        });
  }
}

1.21.4. Scss简介

Vaadin使用Scss作为样式表。Scss是CSS3的扩展,它为CSS添加了嵌套规则、变量、mixins、选择器继承和其他功能。

变量

Scss允许定义可以在规则中使用的变量。

$textcolor: blue;

.v-button-caption {
  color: $textcolor;
}

上面的规则将编译为CSS:

.v-button-caption {
  color: blue;
}
嵌套

Scss支持嵌套规则,这些规则被编译为内部选择器。例如:

.v-app {
  background: yellow;

  .mybutton {
    font-style: italic;
    .v-button-caption {
      color: blue;
    }
  }
}

编译为:

.v-app {
  background: yellow;
}

.v-app .mybutton {
    font-style: italic;
}

.v-app .mybutton .v-button-caption {
  color: blue;
}
Mixins

Mixins是可以被包含在其他规则中的规则。
你可以通过在规则名称前面添加@mixin关键字来定义mixin规则,然后使用@include将其应用于其他规则,同时还可以向其传递参数,这些参数在mixin中作为局部变量处理。
例如:

@mixin mymixin {
  background: yellow;
}

@mixin marginmixin($param) {
  margin: $param;
}

.v-button-caption {
  @include mymixin;
  @include marginmixin(10px);
}

上面的规则将编译为如下的CSS:

.v-button-caption {
  background: yellow;
  margin: 10px;
}
编译Scss

必须将Sass主题编译为浏览器理解的CSS,可以使用Vaadin Sass编译器进行编译,也可以在Eclipse、Maven中编译,也可以在浏览器中加载应用程序时即时运行。

有关Sass的相关文档请参阅: Sass 官方文档

1.22. 系统API的扩展

对于系统API,在项目实施的时候可能不完全满足需求,这时候可以对系统API进行扩展。

1.22.1. 扩展系统的Service

针对系统中任何服务的扩展,均可以以下方式来实现。

定义接口继承自需要扩展service的接口

以UserService为例:添加IExtensionUserService接口继承自IUserService接口:

public interface IExtensionUserService extends IUserService {

    void extensionBusiness();

}
定义扩展类继承自需要的Service类

继续以Uservice为例,添加ExtensionUserService类继承UserService实现IExtensionUserService接口:

@Service
@Primary
public class ExtensionUserService extends UserService implements IExtensionUserService {
    @Override
    public void extensionBusiness() {
        // do something
    }
}
@Primary注解表示有多个实例时,默认使用哪一个,所以必须要加,否则程序会报错。

如果项目对 User 进行了扩展(添加了扩展属性),扩展以后封装了 ExtensionUser 对象,并且希望 ExtensionUserService 中的查询方法返回 List<ExtensionUser>

那么可以做如下封装以便使用:

  1. ExtensionUser 添加带 UserEntity 的构造方法:

    public class ExtensionUser extends User {
        // ...
    
        public ExtensionUser(UserEntity userEntity){
            super(userEntity);
        }
    
        // ...
    }
  2. 在Service中转换:

    import java.util.List;public interface IExtensionUserService extends IUserService {
    
        void extensionBusiness();
    
        List<ExtensionUser> listByAge(int age);
    
    }
    @Service
    @Primary
    public class ExtensionUserService extends UserService implements IExtensionUserService {
        @Override
        public void extensionBusiness() {
            // do something
        }
    
        @Override
        List<ExtensionUser> listByAge(int age){
            EntityFilter filter = createEntityFilter();
            filter.cfFieldGreatorOrEqualTo(ExtensionUser.AGE, age);
            List<UserEntity> userEntities = getEntityHandler().listByFilter(filter);
            List<ExtensionUser> result = new ArrayList<>();
            for(UserEntity userEntity: userEntities){
                result.add(createClientObject(userEntity));
            }
            return createExtensionUsers(userEntities);
        }
    
        @Override
        protected ExtensionUser createClientObject(UserEntity objectEntity) {
            return new ExtensionUser(userEntity);
        }
    
    }

1.22.2. 使用方式

直接注入即可。

@Component
@Scope("prototype")
public class UserViewPresenter extends BasePresenter {

    @Autowired
    private IExtensionUserService extensionUserService;

}

1.23. 系统事件

系统事件是用来在多模块之间进行同步或协同处理对实时性要求不高的业务逻辑的功能, 比如HRM(人事管理系统)录入了新入职的人员信息希望将此信息同步到MES系统中, 这时就可以使用系统事件(SystemEvent)功能,在HRM录入新入职员工信息的时候,创建一个人员添加事件, 然后在MES编写相应代码去轮询该事件以同步人员信息。

1.23.1. 发起端

直接创建SystemEvent对象,设置相应属性值,属性值如下:

属性名称

属性类型

描述

name

String

系统事件的名称

eventType

String

系统事件的类型

content

String

事件的内容,为方便对接,应预先定义好格式

producer

String

事件源,事件发起者

consumer

String

处理者,在事件处理完成时

handleTime

ZonedDateTime

时间处理完成的时间

status

String

事件状态

comment

String

备注

发起端创建SystenEvent是需要填充的属性:name,eventType,content, producer

创建好了之后直接调用SystemEventService的save()方法。

1.23.2. 处理端

处理端需要写轮询查询处理端需要处理的事件。 ISystemEventService提供了多个查找方法,

public interface ISystemEventService extends IBaseDomainObjectService<SystemEvent> {

    /**
     * 根据consumer查找状态为 Created,InProcess的Event对象
     *
     * @param consumer
     * @return
     */
    List<SystemEvent> listActiveEventByConsumer(String consumer);

    /**
     * 根据producer查找状态为Created,InProcess的Event对象
     *
     * @param producer
     * @return
     */
    List<SystemEvent> listActiveEventByProducer(String producer);

    List<SystemEvent> listByConsumer(String consumer);

    List<SystemEvent> listByProducer(String producer);

    /**
     * 根据type和consumer查找Event对象
     *
     * @param type
     * @param consumer
     * @param active   指定查找有效/无效的Event
     * @return
     */
    List<SystemEvent> listByTypeAndConsumer(String type, String consumer, boolean active);

}

在事件处理完成后需要更新事件信息。

1.24. 缓存

平台提供了缓存管理,在做二次开发时能通过注解等简单方式管理缓存。

1.24.1. hibernate缓存

hibernate提供了缓存的功能用于提高性能, 本章节描述了平台使用到的hibernate的缓存技术,具体hibernate缓存的相关技术说明,可以参考 hibernate官方文档

一级缓存

hibernate一级缓存是Session级别的缓存(可以理解为事务范围的缓存),一个Session做了一个查询操作, 它会把这个操作的结果放在一级缓存中,如果短时间内这个session(一定要同一个session)又做了同一个操作, 那么hibernate直接从一级缓存中拿,而不会再去连数据库中取数据, 一级缓存无法取消, 但可以管理,可以使用 session.clear()session.evict() 清除或驱逐,session关闭时缓存失效

二级缓存

hibernate提供了二级缓存用于在sessionFactory级别,默认是关闭的,平台可以按照如下方式开启二级缓存

lumos.commons.data.hibernate-properties.hibernate.cache.use_second_level_cache=true
lumos.commons.data.hibernate-properties.hibernate.cache.region_prefix=${spring.application.name}-hibernate
lumos.commons.data.hibernate-properties.javax.persistence.sharedCache.mode=ENABLE_SELECTIVE
lumos.commons.data.hibernate-properties.hibernate.cache.region.factory_class=com.ags.lumosframework.impl.base.hibernate.internal.EhcacheRegionFactory

Hibernate的二级缓存策略,是针对于ID查询的缓存策略,对于条件查询则毫无作用,为此,Hibernate提供了针对条件查询的查询缓存。 对于需要缓存的对象entity,需要在entity上添加注解@Cacheable

查询缓存
  1. 对于经常使用的查询语句,如果启用了查询缓存,当第一次执行查询语句时,Hibernate会把 查询结果存放在第二缓存中。 以后再次执行该查询语句时,只需从缓存中获得查询结果,从而提高查询性能。

  2. 平台提供的entityFilter有一个 setCacheable 方法用于将该次查询做缓存(前提是entity对象添加了 @Cacheable ) 查询缓存使用的前提是开启了二级缓存.

  3. 查询缓存适用于很少对与查询语句关联的数据库数据进行插入、删除或更新操作语句

  4. 平台可以按照如下方式开启查询缓存

lumos.commons.data.hibernate-properties.hibernate.cache.use_query_cache=true

1.24.2. 平台缓存

虽然hibernate提供了相关的缓存,但也仅仅在该应用上会有性能提升,对于rpc调用的消费端, 由于缓存是在服务提供者那一端的,会导致消费端没有缓存造成性能瓶颈, 所以平台在消费端和提供者端都使用了基于ehcache cluster的缓存技术.

ehcache平台默认是内存的, 如果需要开启集群支持,需要启动terracotta服务, 并在应用配置文件内作如下配置,terracotta的安装使用可以参考部署文档

lumos.commons.ehcache3.cache-heap-size=2 (1)
lumos.commons.ehcache3.cache-off-heap-size=20 (2)
lumos.commons.ehcache3.time_to_live=24 (3)
lumos.commons.ehcache3.cluster-enabled=true
lumos.commons.ehcache3.cluster.default-shared-pool-size=512 (4)
lumos.commons.ehcache3.cluster.node[0].host=127.0.0.1 (5)
lumos.commons.ehcache3.cluster.node[0].port=9410 (6)
lumos.commons.ehcache3.cluster.node[1].host=127.0.0.1
lumos.commons.ehcache3.cluster.node[1].port=9411

lumos.commons.ehcache3.cache-config[com.ags.lumosframework.sdk.entity.UserEntity].cache-heap-size=3 (7)
lumos.commons.ehcache3.cache-config[com.ags.lumosframework.sdk.entity.UserEntity].cache-off-heap-size=30 (8)
lumos.commons.ehcache3.cache-config[com.ags.lumosframework.sdk.entity.UserEntity].time_to_live=18 (9)
1 每个缓存在本地堆内存的大小为2M
2 每个缓存在本地堆外内存的大小为20M
3 每个缓存失效时间,默认为24小时
4 所有缓存共享的资源池总大小为512M (该配置用于配置terracotta的默认资源池大小,所有连接terracotta的应用必须一致)
5 terracotta 连接的ip
6 terracotta 连接的端口
7 (可选项)根据具体的缓存对象定制本地堆内存的大小3M,这里是com.ags.lumosframework.sdk.entity.UserEntity, 如果不配置, 则取默认值Item1的值
8 (可选项)根据具体的缓存对象定制本地堆外内存的大小30M,这里是com.ags.lumosframework.sdk.entity.UserEntity, 如果不配置, 则取默认值Item2的值
9 (可选项)根据具体的缓存对象定制每个缓存失效时间18小时,这里是com.ags.lumosframework.sdk.entity.UserEntity, 如果不配置, 则取默认值Item3的值
第7. 8. 9项同时配置才生效。

平台提供了 @CacheAdd @CacheDelete 注解用于声明式缓存添加, 该类注解可以在IBaseEntityHandler或者ICustomizedObjectSupportService的子类方法上添加

平台也提供了CacheManager类用于编程式调用

  • 使用注解添加缓存,只适用于查询结果为单条数据的情况。即查询唯一确定一条记录时使用缓存注解

  • 该注解只能加在接口方法上。

  • IBaseEntityHandler子类在使用缓存时使用的cache的name为entity class的name

  • ICustomizedObjectSupportService子类在使用缓存时使用的cache的name为自定义表的名称

使用 @CacheAdd 添加单缓存参数
public interface IBaseEntityHandler<T extends IBaseEntity> extends IBaseHandler {
    // ...
    @CacheAdd(key = "'id=' + #id") (1)
    T getById(long id);
    // ...
}
1 key作为表达式,用于计算在缓存中的key值, 表达式语法可以参考spring framework中的spel表达式, 其中#后面跟的是参数的名称, #id表示取方法id值, 示例中表达式将会生成 id=1 类似的key, 1表示id实际的值, 该示例表示会将返回值以 id=1 为key放入缓存,下次调用该方法时会优先从缓存获取
使用 @CacheAdd 添加多缓存参数
public interface IUIConfigurationHandler extends IBaseEntityHandler<UIConfigurationEntity> {
    // ...
    @CacheAdd(key = "'name=' + #name + '&type=' + #type")
    UIConfigurationEntity getByNameAndType(String name, UIConfigurationType type);
    // ...
}
使用 @CacheDelete 移除缓存
public interface IBaseEntityHandler<T extends IBaseEntity> extends IBaseHandler {
    // ...
    @CacheDelete(key = "'id=' + #id") (1)
    void deleteById(long id);
    // ...
}
1 key表达式写法和CacheAdd一样,只是CacheDelete表示移除该缓存
使用CacheManager操作缓存

使用步骤:

  1. 注入 CacheManager ,获取Cache对象。

    @Autowired
    private CacheManager cacheManager;
    //...
    Cache cache = cacheManager.getOrCreateCache("object");
  2. 调用 Cache 中的方法进行添加。

    cache.put("key", value, "aliaName");
  3. 调用 Cache 中的方法进行移除。

    cache.evict("key");
  4. 调用 CacheManager 中的方法清空刚刚获取的cache。

    @Autowired
    private CacheManager cacheManager;
    //...
    cacheManager.clearCache("object");

具体使用方式可以参考下面的api说明

CacheManager 接口定义:
public interface CacheManager {

    /**
     * 根据名称查询或者创建对应的缓存对象
     *
     * @param name
     * @return
     */
    Cache getOrCreateCache(String name);

    /**
     * 根据名称查询或者创建对应的缓存对象
     *
     * @param name
     * @param config
     *            创建缓存时使用的配置对象
     * @return
     */
    Cache getOrCreateCache(String name, Object config);

    /**
     * 清空缓存下的所有记录
     *
     * @param name
     */
    void clearCache(String name);

}
Cache接口定义:
public interface Cache {

    /**
     * 获取缓存的名字
     *
     * @return
     */
    String getName();

    Object getNativeCache();

    /**
     * 根据key获取缓存中的记录
     *
     * @param key
     * @return
     */
    Optional<Object> get(String key);

    /**
     * 返回对应类型的缓存记录
     *
     * @param key
     * @param type
     * @param <T>
     * @return
     */
    <T> Optional<T> get(String key, Class<T> type);

    /**
     * 添加一个缓存记录
     *
     * @param key
     * @param value
     * @param aliasNames
     *            缓存记录key的别名
     */
    void put(String key, Object value, String... aliasNames);

    /**
     * 先移除记录,再更新缓存
     *
     * @param key
     * @param value
     * @param aliasNames
     */
    void evictAndPut(String key, Object value, String... aliasNames);

    /**
     * 移除缓存记录
     *
     * @param key
     */
    void evict(String key);

    /**
     * 清空缓存
     */
    void clear();

}
分布式情况下的缓存时序图
cache sequence
清理缓存

在高級设定-缓存管理可以清理对象的缓存, 包括Hibernate的缓存, 平台的缓存。

cache clear

1.25. 审计功能

平台对内建的表和自定义表做了审计功能的支持, 针对相关的表会创建一个后缀为_A的表作为审计日志存储操作日志, 系统配置里可以选择开启哪些内建表的审计支持 如果开启了审计功能,系统已经针对以下内容自动记录审计信息

  • 内置表的增删改查操作

  • 内置表之间的关联关系操作

  • 自定义表的增删改查操作

1.25.1. 扩展平台支持的内建表

如果有需要将更多的对象加入到内建的支持审计功能,需要实现以下接口

public interface IAuditObjectProvider {

    /**
     * 返回支持的对象类型
     * @return
     */
    List<IObjectType> getSupportedAuditObjects();
}

以下是平台的示例:

public class CorewebAuditObjectProvider implements IAuditObjectProvider {
    @Override
    public List<IObjectType> getSupportedAuditObjects() {
        List<IObjectType> objectTypes = new ArrayList<>();
        //lumos
        objectTypes.add(LumosFrameworkObjectType.CustomizedTableDefinition);
        objectTypes.add(LumosFrameworkObjectType.CustomizedTableColumnDefinition);
        objectTypes.add(LumosFrameworkObjectType.CustomizedTableIndexDefinition);
        objectTypes.add(LumosFrameworkObjectType.CustomizedTableIndexColumn);
        objectTypes.add(LumosFrameworkObjectType.I18NText);
        objectTypes.add(LumosFrameworkObjectType.I18NTextItem);
        objectTypes.add(LumosFrameworkObjectType.Configuration);
        objectTypes.add(LumosFrameworkObjectType.UIConfiguration);
        objectTypes.add(LumosFrameworkObjectType.UIFormElement);
        objectTypes.add(LumosFrameworkObjectType.UIGridColumn);
        //lumos-app
        objectTypes.add(CoreserviceObjectType.User);
        objectTypes.add(CoreserviceObjectType.Role);
        objectTypes.add(CoreserviceObjectType.UserGroup);
        objectTypes.add(CoreserviceObjectType.Doc);
        objectTypes.add(CoreserviceObjectType.SequenceDef);
        objectTypes.add(CoreserviceObjectType.SequenceParamDef);
        objectTypes.add(CoreserviceObjectType.Department);
        objectTypes.add(CoreserviceObjectType.JobPosition);
        objectTypes.add(CoreserviceObjectType.DictionaryType);
        objectTypes.add(CoreserviceObjectType.DataDictionaryItem);
        objectTypes.add(CoreserviceObjectType.Report);
        objectTypes.add(CoreserviceObjectType.ReportParameter);
        objectTypes.add(CoreserviceObjectType.MailTemplate);
        objectTypes.add(CoreserviceObjectType.FreeMarkerTemplate);
        objectTypes.add(CoreserviceObjectType.Project);
        objectTypes.add(CoreserviceObjectType.ProjectItem);
        return objectTypes;
    }
}

1.25.2. 如何给已有对象添加自定义的审计日志

除了系统已经提供的审计信息外,用户可以自己加入更多其他的审计信息,如自定义表的关联关系的维护等。

平台提供了如下的接口, 调用相关的方法可以记录自定义的一些日志内容,也提供了一些查询

public interface IAuditService {

    /**
     * 根据entityClass做审计日志
     *
     * @param entityClass
     * @param objectId    对象的id
     * @param operation   操作类别
     * @param content     记录的对象,最终以json存储
     */
    void logAsJson(Class<?> entityClass, long objectId, String operation, Object content);

    /**
     * 根据自定义表名做审计日志
     *
     * @param tableDefinitionName
     * @param objectId            对象的id
     * @param operation           操作类别
     * @param content             记录的对象,最终以json存储
     */
    void logAsJson(String tableDefinitionName, long objectId, String operation, Object content);

    /**
     * 根据entityClass做审计日志
     *
     * @param entityClass
     * @param objectId    对象的id
     * @param operation   操作类别
     * @param content     记录的对象内容字符串
     */
    void log(Class<?> entityClass, long objectId, String operation, String content);

    /**
     * 根据自定义表名做审计日志
     *
     * @param tableDefinitionName
     * @param objectId            对象的id
     * @param operation           操作类别
     * @param content             记录的对象内容字符串
     */
    void log(String tableDefinitionName, long objectId, String operation, String content);

    /**
     * 根据entityClass做审计日志
     *
     * @param entityClass
     * @param auditDTO
     */
    void log(Class<?> entityClass, AuditDTO auditDTO);

    /**
     * 根据自定义表名做审计日志
     *
     * @param tableDefinitionName
     * @param auditDTO
     */
    void log(String tableDefinitionName, AuditDTO auditDTO);

    /**
     * 根据自定义表名做审计日志
     *
     * @param entityClass
     * @param auditDTOs
     */
    void log(Class<?> entityClass, List<AuditDTO> auditDTOs);

    /**
     * 根据自定义表名做审计日志
     *
     * @param tableDefinitionName
     * @param auditDTOs
     */
    void log(String tableDefinitionName, List<AuditDTO> auditDTOs);

    /**
     * 根据IObjectType类别查询审计日志
     *
     * @param objectType
     * @param objectId
     * @return
     */
    List<AuditDTO> listByObjectTypeAndObjectId(IObjectType objectType, long objectId);

    /**
     * 根据自定义表名查询审计日志
     *
     * @param customizedTableDefinitionName
     * @param objectId
     * @return
     */
    List<AuditDTO> listBycustomizedTableDefinitionNameAndObjectId(String customizedTableDefinitionName, long objectId);
}

1.26. Properties配置文件value加密

Properties配置文件里经常会配置一些敏感信息, 比如密码等。 为安全起见,可以在配置时给值加密。

1.26.1. 生成密文, 有两个方法

方法1、 浏览器输入http://<IP>:<Port>/Jasper/rest/security/encrypt/<明文>

例如:
   输入 http://localhost:8081/Jasper/rest/security/encrypt/123456
   返回:{"code":1,"success":true,"message":"encrypt successful","data":"DJo+L6X9K1w5RdRpNsREmA==","path":null,"exception":null}
   密文:"DJo+L6X9K1w5RdRpNsREmA=="

方法2、 注入StringEncryptor, 直接调用encrypt("你的明文")即可生成密文, 如下代码:

public class EncryptTool{
     @Autowired
     private StringEncryptor encryptor;

     /**
     * 明文是:"123456"
     * 密文password的值是:"a8/dPljhKkZZKnhkzJcahg"
    */
     public void encrypt(){
         String password = encryptor.encrypt("123456");
     }
}
配置文件配置密文
密文必须放入“ENC()”里, 如下
lumos.commons.data.datasource.password=ENC(DJo+L6X9K1w5RdRpNsREmA==)

1.27. 后台任务

后台任务是由lumos平台提供的用于定时或重复执行某段特定业务逻辑的功能,是一个分布式定时任务的功能。

为适应不同的业务场景,平台提供了三种后台任务的添加方式:

  • 使用 @BackgroundJob 注解

  • 实现 IJobType 接口

  • 使用 IQuartzService 中的API手动添加

后台任务的功能依赖于ActiveMQ(artemis)组件

1.27.1. 添加后台任务执行类

无论是使用什么方式添加后台任务,首先都需要添加后台任务执行类,编写后台任务需要执行的业务逻辑。

添加任务类继承 AbstractQuartzJob 类。

示例:

@Component
public class DemoJob extends AbstractQuartzJob {

    private static final Logger log = LoggerFactory.getLogger(DemoJob.class);

    @Override
    public void doExecute(JobExecutionContext context) {
        log.info("=========Job Processing=========");
        log.info("Job Name: " + context.getName());
        Map<String, Object> dataMap = context.getJobDataMap();
        log.info("Job Parameters: " + dataMap);
    }

}
需要确保Spring能够扫描到自定义的任务执行类。

1.27.2. 添加后台任务

在添加了后台任务执行类后,需要将其添加到系统中,使之按指定的规律或时间点执行。正如前文所述,平台提供了三种后台任务的添加方式, 三种方式都有各自的特点,使用时根据实际业务场景适当的选用添加方式。

三种方式的特点:

  • 使用 @BackgroundJob 注解

    使用该方式定义的任务会在服务启动时添加到系统中,且不能使用参数,可以在后台任务管理页面中修改/暂停/删除但不能添加。 当然,可以通过注解参数指定其能否在后台任务管理页面中展示。

  • 实现 IJobType 接口

    使用该方式需要在后台任务管理页面上手动添加任务,可以定义并使用参数。

  • 使用 IQuartzService 中的API

    这种方式是最灵活的,相对的,在使用上也是比较复杂的,如果需要在实际项目中使用API去添加任务,那该方式就是最优的选择。

在介绍具体使用之前,需要说明后台任务的执行方式,总共有两种:repeat和cron,repeat:重复执行,可指定重复间隔及重复次数;cron:根据cron表达式执行。

接下来介绍三种方式的具体使用

  • 使用 @BackgroundJob 注解

    直接在任务执行类上添加 @BackgroundJob 注解。

    以站内信推送任务为例:

    @BackgroundJob(name = "Push.InnerMessage.Task.Name",
            nameI18Nkey = "Push.InnerMessage.Task.Name",
            groupName = "Push.InnerMessage.Task.Group",
            groupNameI18NKey = "Push.InnerMessage.Task.Group",
            description = "Push.InnerMessage.Task.Description",
            descriptionI18NKey = "Push.InnerMessage.Task.Description",
            cronExpression = "0 0/5 * * * ?",
            visible = true
    )
    @Component
    public class InnerMessageQuartzJob extends AbstractQuartzJob {
    
        @Override
        public void doExecute(JobExecutionContext context) throws Exception {
            // 站内信推送逻辑
        }
    
    }

    执行方式的指定:

    • cron:指定 cronExpression 参数值即认为使用cron方式执行任务。

    • repeat:不指定 cronExpression 参数即认为使用repeat方式执行,可以指定 repeatCount (默认是-1,即不限执行次数)、 repeatInterval (默认是1)、 timeUnit (默认是 TimeUnitEnum.Second )

      repeatCount 为重复次数, repeatInterval 为重复间隔, timeUnit 为间隔时间单位。

    参数 visible 指定该任务能否在后台任务管理页面展示,默认为 true

  • 实现 IJobType 接口

    添加任务定义类实现 IJobType 接口

    示例:

    public class DemoJobType implements IJobType {
    
        public static String ID = "DEMO_JOB";
    
        @Override
        public String getId() {
            return ID;
        }
    
        @Override
        public String getJobTypeName() {
            return "Demo Job";
        }
    
        @Override
        public String getJobTypeNameI18Key() {
            return "job.type.demo";
        }
    
        @Override
        public String getJobDescription() {
            return "This job is demo,Tasks can be run automatically at a specified time, as needed, and parameters can be passed";
        }
    
        @Override
        public String getJobDescriptionI18Key() {
            return "job.type.demo.description";
        }
    
        @Override
        public String getJobClazzName() {
            return DemoJob.class.getName();
        }
    
        @Override
        public List<IParameterDef> getQuartzParameters() {
            List<IParameterDef> quartzParameters = new ArrayList<>();
            quartzParameters.add(DemoParameterEnum.scenarioID);
            quartzParameters.add(DemoParameterEnum.startDate);
            quartzParameters.add(DemoParameterEnum.endDate);
            return quartzParameters;
        }
    
    }
    • 在对应项目的META-INF/spring.factories文件中加入相应配置以便其受Spring容器管理:

      com.ags.lumosframework.common.quartz.IJobType=\
      com.ags.lumosframework.common.quartz.DemoParamType
    • 其中的参数可通过实现 IParameterDef 接口进行定义。

      示例:

      public enum DemoParameterEnum implements IParameterDef {
      
          scenarioID("Scenario ID", "job.demo.param.scenarioId", ParameterType.String),
      
          startDate("Start Date", "job.demo.param.startDate", ParameterType.Date),
      
          endDate("End Date", "job.demo.param.endDate", ParameterType.Date);
      
          String name;
          String nameI18NKey;
          ParameterType parameterType;
      
          DemoParameterEnum(String name, String nameI18NKey, ParameterType parameterType) {
              this.name = name;
              this.nameI18NKey = nameI18NKey;
              this.parameterType = parameterType;
          }
      
          @Override
          public String getKey() {
              return getName();
          }
      
          @Override
          public String getName() {
              return name;
          }
      
          @Override
          public String getNameI18NKey() {
              return nameI18NKey;
          }
      
          @Override
          public ParameterType getType() {
              return parameterType;
          }
      }
      如果参数是自定义类型,那么自定义参数类必须实现 Serializable 接口,必须指定 serialVersionUID
  • 使用 IQuartzService 中的API

    直接调用 IQuartzService 中的接口即可,接口的详细介绍请查看API文档。

    如果参数是自定义类型,那么自定义参数类必须实现 Serializable 接口,必须指定 serialVersionUID

1.28. 数据权限

根据用户所在的部门或者工厂建模架构决定用户访问数据的权限,即提供两种模式的数据权限控制

  • 基于用户所在部门的数据权限控制

    通过OC来控制用户的的数据访问权限

  • 基于用户所在的工厂建模架构的数据权限控制

    通过工厂的拓扑结构控制用户的数据访问权限

两种模式只能择其一,一旦确定后,在系统使用过程中不能切换。

1.28.1. 配置权限管控模式

#数据权限的配置项,可选项:[mes|OC|off] 默认关闭
# mes: 基于用户所在工厂建模架构的数据权限控制
# OC: 基于用户所在部门的数据权限控制
# off: 关闭数据权限控制
lumos.data.permission.mode=OC

1.28.2. 数据库表信息

对于需要做数据隔离及数据权限管控的对象,在创建表时需要额外增加一个字段 DATA_PERMISSION_PREDICATE 用于存放数据权限代码。

<column name="DATA_PERMISSION_PREDICATE" type="varchar(255)"/>
所有继承BaseEntity类的对象,默认应用数据权限,在定义数据库表的时候,必须添加DATA_PERMISSION_PREDICATE字段

开启权限模式后,当保存或者修改继承了baseEntity的类的对象时,会将当前用户对应的权限码保存在该条记录的 DATA_PERMISSION_PREDICATE中 ,下次查询的时候就会将此字段作为查询条件进行筛选

1.28.3. 使用

开启平台提供的数据权限功能后,使用BaseEntityDao的list方法及使用EntityFilter查询都是默认加上数据权限进行检索, 但如果直接使用SQL或者HQL查询数据,则需要手动加上数据权限作为检索条件。

示例:

public class DeparmentDao{
    public List<DepartmentEntity> listRootDepartment() {
        String hql =
            "select dept from DepartmentEntity dept left join DepartmentEntity parentDept on dept.parentDepartmentCode=parentDept.departmentCode "
                + " where (dept.departmentCode is null or parentDept.departmentCode is null) ";

        // 手动加上数据权限作为检索条件
        if(DataPermissionConfManager.isAddDataPermission(DepartmentEntity.class)){
            List<String> dataPermissionList = RequestInfo.current().getDataPermissionList();
            hql = hql + " and " + DataPermissionConfManager.buildDataPermissionPredicate("dept",dataPermissionList);
        }

        return baseDao.listByJpql(hql, DepartmentEntity.class);
    }
}

想要获取工厂建模对象或者部门对象对应的权限码,只需要调用 getDataPermissionCode 方法

1.28.4. 数据权限自定义

在实际项目中可能大部分Entity都是继承自BaseEntity,但又不需要做数据权限管控,对于这种需求,平台提供了两种方式去掉对象数据的权限管控。

  1. 使用 @IgnoreDataPermission 注解

    该方式可以对具体项目中的Entity进行过滤

  2. 实现 IDataPermissionFilter 接口

    该方式可对平台提供的Entity进行过滤

  • 使用 @IgnoreDataPermission 注解

    在Entity类中添加@IgnoreDataPermission注解。

    示例:

    @IngoreDataPermission
    public class I18NTextEntity extends BuildtimeBaseEntity {
        // ...
    }
  • 实现 IDataPermissionFilter 接口

    可以实现 IDataPermissionFilter 接口,排除数据权限的检查

    示例:

    @Component
    public class MyDataPermissionFilter implements IDataPermissionFilter{
    
        @Override
        public List<Class> listExcludeEntityClazz(){
            List<Class> list = new ArrayList<>();
            list.add(User.class);
            list.add(Route.class);
            return list;
        }
    }
    确保实现类能够被Spring扫描到。

为了方便理解,可以观看视频:

1.29. 事件注册中心

事件注册中心是平台提供用于管理事件触发/监听的功能,可以在集群环境下跨JVM进行事件监听,比如可以在独立运行的LSH上监听工单完成、班次的开始/结束等。

同时,平台事件监听支持 组(Group) ,效果如下图,具体如何使用请查看事件监听

  1. 使用组:在同一个组里面,只有一个节点能监听到事件。

  2. 不使用组:所有节点都能监听到事件。

700

一般情况,事件模块都有事件触发端及事件监听端,平台提供的事件注册功能也不例外,接下来就从这两个方面来介绍如何使用平台提供的事件注册中心功能。

1.29.1. 事件触发

  1. 定义一个Event类

    定义一个简单的java类作为一个Event类。

    以MES中的班次开始事件为例:

    public class WorkScheduleStartEvent implements Serializable {
    
        private static final long serialVersionUID = -1L;
    
        private long workScheduleId;
    
        private ZonedDateTime startTime;
    
        List<Long> shiftInstanceIds;
    
        // set/get方法
    }
    自定义的Event类必须实现 Serializable 接口,必须指定 serialVersionUID
  2. 触发事件

    注入 EventBus 实例,调用 publish(Object event) 方法触发事件。

    以MES中的班次开始事件为例:在班次开始时触发班次开始事件

    @Component
    public class ShiftQuartzJob extends AbstractQuartzJob {
    
        @Autowired
        private EventBus eventBus;
    
        @Override
        public void doExecute(JobExecutionContext context) {
            // ...
            // 触发班次开始(上班)事件
            WorkScheduleStartEvent startEvent = new WorkScheduleStartEvent();
            startEvent.setStartTime(ZonedDateTime.now());
            startEvent.setWorkScheduleId(workSchedule.getId());
            startEvent.setShiftInstanceIds(shiftInstanceIds);
            eventBus.publish(startEvent);
        }
    }

1.29.2. 事件监听

平台事件注册中心提供了两种注册监听的方式:

  1. 调用API动态的添加事件监听

    使用这种方式能够灵活的控制事件监听的添加及删除。使用方式如下

    1. 定义监听器类实现 IEventListener 接口(示例中使用匿名类)。

      EventListener 接口定义

      public interface EventListener<T> {
      
          void handle(T event);
      
          default boolean isGroupedEvent() {
              return false;
          }
      }
      • handle(T event) 监听回调。

      • isGroupedEvent() 返回true时表示同一个组(集群环境)下只有一个节点能收到事件,false则表示所有节点都能收到。

      • 接口中的泛型即为事件类,直接使用上一节中定义的事件类即可。

    2. 注入 EventBus 实例,调用 register() 方法注册监听。

      示例:

      public class EventListenerView extends BaseView {
      
          private Button btnAddEvent = new Button("Add Event");
      
          private TextArea taContent = new TextArea();
      
          // 注入EventBus实例
          @Autowired
          private EventBus eventBus;
      
          @Autowired
          private IWorkScheduleService workScheduleService;
      
          public EventListenerView() {
              // ...
              btnAddEvent.addClickListener(event -> {
                  // 调用register()方法注册监听
                  eventBus.register(new EventListener<WorkScheduleStartEvent>() {
      
                      @Override
                      public void handle(WorkScheduleStartEvent startEvent) {
                          System.out.println(">.前台页面收到监听消息!!!!!_");
                          WorkSchedule workSchedule = workScheduleService.getById(startEvent.getWorkScheduleId());
                          getUI().access(()->{
                              taContent.setValue(workSchedule.getName() + "\n" + startEvent.getStartTime());
                          });
                      }
                  });
              });
              // ...
          }
      }
  2. 定义好事件监听器后在启动时自动添加

    使用这种方式能够简便快捷的定义一个事件监听器,使之在服务启动时自动开启监听。使用方式如下:

    定义监听器类实现 EventListener 接口,添加 @Component 注解。

    示例:监听班次开始

    @Component
    public class DemoWorkScheduleStartListener implements EventListener<WorkScheduleStartEvent> {
    
        @Autowired
        private IWorkScheduleService workScheduleService;
    
        @Override
        public void handle(WorkScheduleStartEvent startEvent) {
            long workScheduleId = startEvent.getWorkScheduleId();
            WorkSchedule workSchedule = workScheduleService.getById(workScheduleId);
            System.out.println("班次:" + workSchedule.getName() + " 开始了");
        }
    }
    需要确保监听器在Spring容器的扫描路径下。

1.29.3. 平台自带的事件

Lumos提供了用户登录相关的事件:

  • 用户登录相关事件

    • UserLoginEvent:用户登录

    • UserLogoutEvent:用户登出

MES提供了WorkSchedule及WIP两类的事件

  • WorkSchedule 相关事件

    • WorkScheduleStartEvent:班次开始

    • WorkScheduleEndEvent:班次结束

  • WIP 相关事件

    • WIPStatusChangeEvent:工单状态更新, 包含创建,发布,完成,关闭

    • WorkOrderItemStatusChangeEvent:工单项状态更新,包含完成,关闭

    • WIPStatusChangeEvent:WIP状态更新,包含完成,关闭

1.30. AbstractTreeDataProvider 相关使用说明

1.30.1. 基础说明

AbstractTreeDataProvider 基于 VAADIN TreeDataProvider 实现的抽象provider,扩展了`系统对象` 和`DTO` 的条件查询功能。AbstractTreeDataProvider 不关注于数据对象的形式和要求,但是在扩展时需要实现相关的抽象方法。

1.30.2. 问题

由于 TreeDataProvider 是实现 InMemoryDataProvider 的一种提供类,因此提供的API都是需要基于内存的SerializablePredicate 方法。如果是外部调用最好扩展于 HierarchicalConfigurableFilterDataProvider

1.30.3. 使用说明

  1. 创建一个Provider 继承于AbstractTreeDataProvider<T, ID, F extends IFilter> ,并且指明T,ID,F的类型:

    • T 是数据对象,即`系统对象`和`DTO对象`

    • ID 是数据对象唯一标识的类型

    • F 是继承于 IFilter 的过滤器对象

例子:

@Component
@Scope("prototype")
public class NewPermissonDataProvider extends AbstractTreeDataProvider<PermissionWrapper, Integer, NewPermissonQO> {
    @Override
    protected Collection<PermissionWrapper> listAll() {
        PermissionFactory permissionFactory = BeanManager.getService(PermissionFactory.class);
        List<PermissionWrapper> permissionWrappers = permissionFactory.generateAllPermissionWrapper();
        List<PermissionWrapper> collect = flattenAll(permissionWrappers).collect(Collectors.toList());
        return collect;
    }

    private Stream<PermissionWrapper> flattenAll(Collection<PermissionWrapper> parent) {
        return Stream.concat(parent.stream(),
                parent.stream().filter(pw -> pw.getChildren() != null)
                        .flatMap(pw -> flattenAll(pw.getChildren())));
    }

    @Override
    protected Collection<PermissionWrapper> doFilteredCollection() {
        Optional<List<PermissionWrapper>> optional = Optional.ofNullable(getFirstStepFilter())
                .map(BaseTreeGridQO::getFilterItems)
                .map(p -> {
                    Stream<PermissionWrapper> stream = this.getAll().stream();
                    for (Predicate<PermissionWrapper> permissionWrapperPredicate : p) {
                        stream = stream.filter(permissionWrapperPredicate);
                    }
                    return stream.collect(Collectors.toList());
                });
        if (!optional.isPresent()) {
            return this.getAll();
        }
        return optional.get();
    }

    @Override
    protected Predicate<PermissionWrapper> sameWith(PermissionWrapper source) {
        return target -> target.equals(source);
    }

    @Override
    protected Function<PermissionWrapper, Integer> treeNodeId() {
        return PermissionWrapper::hashCode;
    }

    @Override
    protected Function<PermissionWrapper, Integer> treeNodeParentId() {
        return permissionWrapper -> Optional.ofNullable(permissionWrapper.getParent()).map(PermissionWrapper::hashCode).orElse(null);
    }
}

继承类需要实现相关的抽象方法:

  • listAll() : 获取全部数据的方法,对于返回的对象是`flatten`的,如果原始数据是层级对象,像例子中请自写一个`flatten` 方法。

  • ` doFilteredCollection()` 根据过滤器(可通过`getFirstStepFilter()` 获得)进行数据过滤的逻辑,返回一个过滤后的数据集合。

  • sameWith() 提供一个判断是相同对象的Predicate

  • treeNodeId() 提供获取数据节点中唯一id的方法

  • treeNodeParentId() 提供获取数据节点中父节点id的方法

  1. 实现或者指定相关的`IFilter`对象

对于系统对象有通用的`EntityFilter`作为提供。对于dto对象需要继承 BaseTreeGridQO,并自定义过滤条目,上例中的 NewPermissonQO 实现如下:

public class NewPermissonQO extends BaseTreeGridQO<PermissionWrapper> {
    @Override
    public Map<String, Function<Object, Predicate<Object>>> getFilterMap() {
        return new HashMap<String, Function<Object, Predicate<Object>>>() {{
            put("name", source -> target -> ((String) source).contains((String) target));
            put("nameI18NValue", source -> target -> ((String) source).contains((String) target));
        }};
    }
}

继承 BaseTreeGridQO 后需要实现 `getFilterMap()`方法,返回一个和dto中get方法和Predicate 表达式的映射。以上是提供 名称和i18n值对查询值的包含过滤。

  1. 在开发中指定选择的provider 并且 指定相关的流程(同GRID)如下图:

..\images\core\treegrid filtered provider\process
  1. 系统对象的demo实现

@Component
@Scope("prototype")
public class NewDpmtTreeDataProvider extends AbstractTreeDataProvider<Department, String,EntityFilter> {

    private static final long serialVersionUID = 8274706789795529914L;

    public NewDpmtTreeDataProvider() {
        setFirstStepFilter(new EntityFilter(DepartmentEntity.class));
    }


    @Override
    protected Set<Department> listAll() {
        IDepartmentService service = BeanManager.getService(IDepartmentService.class);
        EntityFilter filter = service.createFilter();
        return service.listByFilter(filter).stream().filter(distinct()).collect(Collectors.toSet());
    }

    @Override
    protected Collection<Department> doFilteredCollection() {
        IDepartmentService service = BeanManager.getService(IDepartmentService.class);
        EntityFilter entityFilter = service.createFilter().appendQuery(getFirstStepFilter());
        return service.listByFilter(entityFilter);
    }

    @Override
    protected Predicate<Department> sameWith(Department source) {
        return department -> Objects.equals(department.getDepartmentCode(), source.getDepartmentCode());
    }

    @Override
    protected Function<Department, String> treeNodeId() {
        return Department::getDepartmentCode;
    }

    @Override
    protected Function<Department, String> treeNodeParentId() {
        return Department::getParentDepartmentCode;
    }


    static Predicate<Department> distinct() {
        Map<Object, Boolean> seen = new ConcurrentHashMap<>();
        return t -> seen.putIfAbsent(t.getDepartmentCode(), Boolean.TRUE) == null;
    }

}

1.31. 自定义domain扩展

自定义domain扩展,旨在继承原有系统对象domain后,二次开发界面获取新的domain属性,增强了domain扩展性。

1.31.1. 用法

以User对象为例

  1. 创建 自定义domain

    UserExtra 继承原始的 User 并且需要实现标记接口 IDomainExtend , 在`UserExtra`自定义方法如下:

    public class UserExtra extends User implements IDomainExtend {
    
        public UserExtra(UserEntity entity) {
            super(entity);
        }
    
        public String getAA() {
            return getCEFAsString("AA");
        }
    
        public String getBB() {
            return getCEFAsString("BB");
        }
    }
  2. 创建自定义的服务层

    如下 标记 @Primary 服务为主Bean,重写 createClientObject createClientObjects,正常来讲只需重写 createClientObject 即可

    @Service
    @Primary
    public class UserExtraService extends UserService {
    
        @Override
        protected UserExtra createClientObject(UserEntity objectEntity) {
            return new UserExtra(objectEntity);
        }
    
    //    @Override
    //    protected List<User> createClientObjects(List<UserEntity> entities) {
    //        return entities.stream().map(this::createClientObject).collect(Collectors.toList());
    //    }
    
    }
  3. 程序启动器标记Domain所在的基础包

    配置 @ExtendPackageScan 标记domain所在的package,请放在 spring的bean注册路径下面,下例是放在spring启动器上

    @SpringBootApplication
    @ExtendPackageScan("com.ags")
    public class MesStandalone {
        public static void main(String[] args) {
            SpringApplication.run(MesStandalone.class, args);
        }
    }

1.32. ELK说明

ELK 是Elasticsearch、Logstash、Kiban三个开源软件的组合。在实时数据检索和分析场合,三者通常是配合共用

想要在项目中使用ELK系统,需要在pom.xml文件中添加如下依赖

<dependency>
    <groupId>com.ags.lumosframework</groupId>
    <artifactId>lumos-monitor</artifactId>
</dependency>

之后在application.properties文件中添加如下三个属性

monitor.app-id=${spring.application.name}
monitor.instance-id=your instance id
monitor.logging.logstash-url=your logstash url

2. lumos 插件项目的二次开发

平台本身提供插件式开发,旨在将可变的功能,用户需求多变的功能,可以通过插件式开发的形式,来扩展平台的功能。

插件开发目前广泛使用的场景为过站界面,因为过站界面需要根据现场业务的逻辑发生变化,所以众多企业要求是不一样的。所以,我们可以通过一系列的插件,来定制客户需要的功能。另外,随着插件的增多,用户的可选择性也会变大,可以使平台的功能更加完善。

其实一切独立可变的功能都可以使用该系统来实现,如WMS的收料,IQA等,如各种系统的看板等功能。

2.1. 环境安装

2.1.1. 平台依赖

  1. 开发集成环境(Eclipse,IDEA)

  2. Maven3.6

  3. Lumos 3.0 及以上

2.2. 定制项目中如何引用

在项目中使用插件非常简单,只需要引入对应的jar包即可。

2.2.1. 定制项目sdk项目依赖引入

在项目父模块中引入stage父模块依赖以确定版本号信息

    <dependencyManagenemnt>
        ......
        <dependency>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-stage-parent</artifactId>
        <version>3.2.0</version>
        <type>pom</type>
        <scope>import</scope>
        </dependency>
        ......
    </dependencyManagenemnt>

将如下依赖加入定制项目的SDK(也就是接口定义项目)中。

        <dependency>
            <groupId>com.ags.lumosframework</groupId>
            <artifactId>lumos-stage-sdk</artifactId>
        </dependency>

2.2.2. 定制项目后端依赖引入

将如下依赖加入定制项目的后端服务实现中。

    <dependency>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-stage-impl</artifactId>
    </dependency>

2.2.3. 定制项目前端依赖引入

将如下依赖加入定制项目的后端服务实现中。

    <dependency>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-stage-vaadin</artifactId>
    </dependency>
如果定制项目的项目接口没有这么详细的划分,比如说所有的项目实现都在一个项目中,那么将上述依赖全局加入这个项目的pom文件即可。

2.2.4. 配置界面进入

一旦加入上述的依赖,重新启动程序后,你将会看到如下界面

operationcenter

该界面用于插件的具体展示界面,是使用者的入口。该使用者包含产线操作工,仓库管理人员,以及看板等。

如果需要创建插件,可以进入插件定义窗口,在该界面,可以进行插件的增删改查等工作,也可以预览插件,如下图所示:

stageCRUD

2.2.5. 插件开发

基类

插件开发相对比较简单,在引入具体的依赖后,你将会看到如下抽象类 “AbstractCustomizedComponent”,该抽象类是所有插件的基类,创建新的类,继承自该类即可。

public abstract class AbstractCustomizedComponent extends BaseComponent {

    /**
     *
     */
    private static final long serialVersionUID = 3780261730528717346L;

    private String name;

    private Map<String, ParameterInstance> instanceMap = new HashMap<>();

    private boolean preview = false;

    public AbstractCustomizedComponent() {

    }

    public boolean isPreview() {
        return preview;
    }

    /**
     * 判定当前程序是否是预览界面,如果是预览界面,将不做数据的初始化。
     *
     * @param preview
     */
    public void setPreview(boolean preview) {
        this.preview = preview;
    }

    @PostConstruct
    private void initParameterInstance() {
        List<ParameterDefinition> parameterDefinitions = CustomizedComponentHelper.getDefinitions(this.getClass());
        parameterDefinitions.forEach(parameterDefinition -> {
            ParameterInstance parameterInstance =
                new ParameterInstance(parameterDefinition, parameterDefinition.readValue(this));
            instanceMap.put(parameterDefinition.getName(), parameterInstance);
        });
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    /**
     * 在插件页面添加插件时,该名称作为国际化的名称,展示给用户,所以一般情况下,你可以返回一个国际化的值。
     * <p>
     * 如在过站界面,系统将返回WorkStation.Runtime.Name。通过该Key,系统可以获得国际化的值,显示给用户
     *
     * @return
     */
    public abstract String getDisplayValue();

    @Override
    public String toString() {
        return getDisplayValue();
    }

    /**
     * 获得该插件所支持的所有参数,这些参数可以提供给用户在界面做配置,然后插件开发者,可以根据这些设定,来定义该插件的行为。
     *
     * @return
     */
    public Collection<ParameterInstance> getParameterInstances() {
        return instanceMap.values();
    }

    /**
     * internal use
     *
     * @param stage
     */
    public void initParameter(Stage stage) {
        Map<String, Object> parameters = stage.getParameters();
        parameters.forEach((key, value) -> {
            ParameterInstance parameterInstance = instanceMap.get(key);
            if (parameterInstance != null) {
                parameterInstance.setValue(value);
                parameterInstance.getParameterDefinition().writeValue(this, value);
            }
        });
    }

    public void _enter() {
        activateParameters();
        enter();
    }

    /**
     * 进入该插件时调用该方法,可以用于除页面之外的数据初始化操作
     */
    public abstract void enter();

    /**
     * 当用户在界面设定了插件开发者的参数,然后在打开界面(或者预览)的时候,如果系统加载到了参数信息,将调用这个方法。
     * <p>
     * 一个典型的应用,比如开发者提供了一个参数,允许用户选择工位,那么一旦工位在插件配置界面被选择后,当加载完这个参数后,会调用该方法,按照用户选择的工位进行动作。
     */
    public abstract void activateParameters();

    public StageCategory getStageCategory() {
        return StageCategory.OPERATION_CENTER;
    }

}
参数指定

插件支持参数功能,参数主要是由插件开发者提供,允许用户在界面设定,然后插件开发者根据插件的参数,在插件中作出不同的反应。

如下代码片段,则是定义了一个插件,并且提供了一些参数:

@UIScope
@SpringComponent
@StagePluginDefinition(displayValue = "WorkStation.DisplayName") (1)
public class RuntimeWorkStationView extends AbstractCustomizedComponent implements ClickListener {

    private static final long serialVersionUID = 2448127387625413009L;

    @ParameterDef(nameKey = "WorkStation.Packing.Parameter.SOPShow", dataType = DataType.Boolean)
    private boolean isMpiShow = true;

    @ParameterDef(nameKey = "WorkStation.Packing.Parameter.DataCollectionShow", dataType = DataType.Boolean)
    private boolean isDatacollectionShow = true;

    @ParameterDef(nameKey = "WorkStation.Packing.Parameter.UploadPhotoShow", dataType = DataType.Boolean)
    private boolean isUploadPhotoShow = true;

    @ParameterDef(nameKey = "WorkStation.Packing.Parameter.QTAGShow", dataType = DataType.Boolean)
    private boolean isQtagShow;

    @ParameterDef(nameKey = "WorkStation.Packing.Parameter.SCRAPShow", dataType = DataType.Boolean)
    private boolean isScrapShow;

    @ParameterDef(nameKey = "WorkStation.Packing.Parameter.PrintSequenceShow", dataType = DataType.List,
        provider = PrinterProvider.class)
    private String printerName;
}

<1>代表是系统中过站插件的注释

如果不想显示平台的插件, 可以实现接口, 如下

@Component
public class BaseRuntimeStagePluginExtendedImple implements BaseRuntimeStagePluginExtended {

    @Override
    public List<String> excludtion() {
        List<String> excludeList = new ArrayList<>();
        excludeList.add(RuntimeWorkStationView.class.getName());
        return excludeList;
    }
}
注意:每个插件的组件上面必须要有 StagePluginDefinition 注解才能被系统扫描到。系统会根据这个注解在插件配置界面显示该插件的配置信息。该注解也就像是插件的识别码或者身份证。
`StagePluginDefinition` 注解有四个参数可配置,代码如下:
@Target(value = {ElementType.TYPE})
@Retention(RUNTIME)
public @interface StagePluginDefinition {

    String className() default "";

    //显示的名称
    String displayValue() default "";

    //显示的名称的国际化Key
    String displayValueKey() default "";

    //插件类型,目前有两种类型,一是“操作中心”,二是“看板”
    StageCategory categoryName() default StageCategory.OPERATION_CENTER;
}

正如你所看到的那样,参数提供非常简单,只需要一个注解即可 “@ParameterDef”,具体内容如下。

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ParameterDef {

    /**
     * 国际化的参数的名称信息
     *
     * @return
     */
    String nameKey();

    /**
     * 该参数的数据类型
     *
     * @return
     */
    DataType dataType();

    /**
     * 在数据类型为List的情况下,系统会自动注入一个Combo Box,列出选项供用户选择,该参数提供下拉列表里的数据集合。
     *
     * @return
     */
    String[] options() default {};

    /**
     * 在数据类型为List的情况下,如果下拉列表是一个动态的值,需要实时到数据库中查询的,如列出所有的工位信息,那么可以实现一个ParameterValueProvider传入
     *
     * @return
     */
    @SuppressWarnings("rawtypes")
    Class<? extends ParameterValueProvider> provider() default ParameterValueProvider.class;
}

系统还为集成系统中的对象提供了便利,例如,可以在定义插件参数的时候,将系统中的全部工位信息提供出来,供用户选择,那么这个就不能在定义插件的时候,预先定义。因为这些事动态变化的值。 用户只需要从以下接口继承就好。

@FunctionalInterface
public interface ParameterValueProvider<T> extends Supplier<List<T>> {

}

如下是一个简单实现,供参考:

public class WorkStationProvider implements ParameterValueProvider<String> {

    @Override
    public List<String> get() {
        IWorkStationService workStationService = BeanManager.getService(IWorkStationService.class);
        List<WorkStation> workstation = workStationService.list(0, Integer.MAX_VALUE);
        return workstation.stream().map(WorkStation::getName).collect(Collectors.toList());
    }
}

一旦用户定义了如上的参数,那么在插件配置界面,就可以看到如下参数配置信息:

stageParameters
开发示例
@UIScope
@SpringComponent
@StagePluginDefinition(displayValue = "WorkStation.Packingstation.Packing")
public class PackingStationView extends AbstractCustomizedComponent implements ClickListener {

    private static final long serialVersionUID = -7578836084244167502L;

    @ParameterDef(nameKey = "WorkStation.Packing.Parameter.IsPrintNeeded", dataType = DataType.Boolean)
    private boolean isPrintNeeded = false;

    @ParameterDef(nameKey = "WorkStation.Packing.Parameter.PackagePrinter", dataType = DataType.List,
        provider = PrinterProvider.class)
    private String packagePrinter;

    // @ParameterDef(nameKey = "WorkStation.Packing.Parameter.PackageType", dataType = DataType.List,
    // provider = PackageTypeProvider.class)
    // private String packageType;

    @Inject
    private PackingStationViewPresenter presenter;

    @I18Support(caption = "Barcode:", captionKey = "WorkStation.Barcode")
    private TextField tfBarcode = new TextField();

    @I18Support(caption = "Scan the SN", captionKey = "WorkStation.InputTips")
    private InputLabel lblInputTips = new InputLabel();

    @I18Support(caption = "Seal", captionKey = "WorkStation.Package.Seal")
    private Button btnSeal = new Button();

    @I18Support(caption = "Complete", captionKey = "WorkStation.Complete")
    private Button btnComplete = new Button();

    @I18Support(caption = "Print", captionKey = "WorkStation.Print")
    private Button btnPrint = new Button();

    private Button[] topTools = {btnSeal, btnComplete, btnPrint};

    private HorizontalLayout hlTips = new HorizontalLayout();

    private TabSheet tabSheet = new TabSheet();

    private Tab packingTab;

    @Inject
    private IPackingScheduleTab scheduleTab;

    @Inject
    private IPackingTab packingTabContent;

    @Inject
    private IApproveConfirmDialog approveConfirmDialog;

    public PackingStationView() {
        VerticalLayout vlRoot = new VerticalLayout();
        vlRoot.setSpacing(false);
        vlRoot.setMargin(false);
        vlRoot.setSizeFull();

        HorizontalLayout hlToolBox = new HorizontalLayout();
        hlToolBox.setWidth("100%");
        hlToolBox.setSpacing(true);
        hlToolBox.setMargin(true);
        hlToolBox.addStyleName(CoreTheme.TOOLBOX);
        vlRoot.addComponent(hlToolBox);

        HorizontalLayout hlTempToolBox = new HorizontalLayout();
        hlToolBox.addComponent(hlTempToolBox);
        hlTempToolBox.addStyleName(CoreTheme.INPUT_DISPLAY_INLINE);
        hlTempToolBox.addComponent(tfBarcode);
        tfBarcode.addStyleName(CoreTheme.BACKGROUND_YELLOW);
        hlTempToolBox.addComponent(hlTips);
        hlTips.addStyleName(CoreTheme.STAGE_LAYOUT_TIPS + " " + CoreTheme.BACKGROUND_ORANGE);

        hlTips.addComponent(lblInputTips);
        for (Button btn : topTools) {
            hlTempToolBox.addComponent(btn);
            // btn.setWidth(120, Unit.PIXELS);
            btn.addClickListener(this);
            btn.setDisableOnClick(true);
        }

        btnSeal.setIcon(VaadinIcons.PACKAGE);
        btnPrint.setIcon(VaadinIcons.PRINT);
        btnComplete.setIcon(VaadinIcons.CHECK);

        // TabSheet
        tabSheet.setSizeFull();
        tabSheet.addStyleNames(ValoTheme.TABSHEET_FRAMED, CoreTheme.JASPER_TABSHEET);
        vlRoot.addComponent(tabSheet);
        vlRoot.setExpandRatio(tabSheet, 1);

        this.setSizeFull();
        this.setCompositionRoot(vlRoot);
        setElementsId();
    }

    private void setElementsId() {
        tfBarcode.setId("tf_barcode");
        lblInputTips.setId("ilbl_inputtips");
        btnSeal.setId("btn_seal");
        btnComplete.setId("btn_complete");
        btnPrint.setId("btn_print");
        hlTips.setId("hl_tips");
    }

    @Override
    public void init() {
        tabSheet.addSelectedTabChangeListener(new SelectedTabChangeListener() {
            private static final long serialVersionUID = -3265758415179285552L;

            @Override
            public void selectedTabChange(SelectedTabChangeEvent event) {
                if (event.getComponent() != null && event.getTabSheet().getSelectedTab() instanceof IPackingTab) {
                    tfBarcode.focus();
                }
            }
        });
        packingTab =
            tabSheet.addTab((Component)packingTabContent, I18NUtility.getValue("WorkStation.Packing", "Packing"));
        packingTab.setId("tab_packing");
        tabSheet.addTab((Component)scheduleTab, I18NUtility.getValue("WorkStation.Schedule", "Schedule"))
            .setId("tab_schedule");
        tabSheet.setSelectedTab((Component)scheduleTab);
        tfBarcode.addShortcutListener(new ShortcutListener(null, KeyCode.ENTER, null) {
            private static final long serialVersionUID = 1L;

            @Override
            public void handleAction(Object sender, Object target) {
                try {
                    String value = tfBarcode.getValue();
                    processSNInput(value);
                } catch (Exception e) {
                    handlingException(e);
                } finally {
                    tfBarcode.setValue("");
                }
            }

        });
        scheduleTab.addScheduleItemClickListener(new ScheduleItemClickListener() {

            @Override
            public void scheduleItemClick(WIP<?> wip) {
                try {
                    processSNInput(wip.getSerialNumber());
                } catch (Exception e) {
                    tfBarcode.setValue("");
                    handlingException(e);
                }
            }
        });

    }

    private void processSNInput(String serialNumber) {
        if (serialNumber != null && !serialNumber.equals("")) {
            if (btnComplete.isEnabled() && serialNumber.equals(presenter.getCompleteBarcode())) {
                clickCompleteBtn();
                return;
            }
            tabSheet.setSelectedTab(packingTab);
            packingTabContent.checkPackageDefSelect();
            if (packingTabContent.isSealed()) {

                throw new PlatformException(
                    I18NUtility.getValue("WorkStation.Packing.BoxSealed", "The box is sealed!"));
            }
            if (isStartedPackage(serialNumber)) {
                setEnable(btnPrint, true);
                presenter.setPacking(true);
                updateButtonStatus();
                return;
            }
            packingTabContent.addSubInstance(serialNumber);
            if (packingTabContent.isSealed()) {
                presenter.print(packagePrinter, packingTabContent.getPackageInstance(),
                    packingTabContent.getSubPackageInstance());
                updateButtonStatus();
            }
            presenter.setPacking(true);
            updateButtonStatus();
            NotificationUtils.notificationInfo(I18NUtility.getValue("Common.Success", "Success"));
        }
    }

    private boolean isStartedPackage(String value) {
        PackageDefinition definition = packingTabContent.getPackageDefinition();
        Package parentPackage = presenter.getParentPackage(value);
        if (Objects.isNull(parentPackage)) {
            return false;
        } else {
            if (definition != null) {
                if (!parentPackage.getPackageDefinition().equals(definition)) {
                    return false;
                }
            }
            packingTabContent.setPackageInstance(parentPackage);
            return true;
        }
    }

    private void updateButtonStatus() {
        btnSeal.setEnabled(presenter.isPacking() && !packingTabContent.isSealed());
        btnComplete.setEnabled(presenter.isPacking());
        setEnable(btnPrint, presenter.isPacking());
    }

    @Override
    public void buttonClick(ClickEvent event) {
        event.getButton().setEnabled(true);
        try {
            if (event.getButton().equals(btnSeal)) {
                packingTabContent.sealPackage();
                presenter.print(packagePrinter, packingTabContent.getPackageInstance(),
                    packingTabContent.getSubPackageInstance());
                updateButtonStatus();
            } else if (event.getButton().equals(btnComplete)) {
                packingTabContent.checkPackageSealed();
                Package packageInstance = packingTabContent.getPackageInstance();
                presenter.complete(packageInstance);
                presenter.setPacking(false);
                scheduleTab.refresh();
                clearAll();
            } else if (event.getButton().equals(btnPrint)) {
                approveConfirmDialog.setPrivalege(MesPrivilegeConstants.PACKING_OFFLINE_PRINT);
                approveConfirmDialog.show(getUI(), new DialogCallBack() {

                    @Override
                    public void done(ConfirmResult result) {
                        if (ConfirmResult.Result.OK.equals(result.getResult())) {
                            packingTabContent.checkPackageSealed();
                            presenter.print(packagePrinter, packingTabContent.getPackageInstance(),
                                packingTabContent.getSubPackageInstance());
                        }
                    }
                });
            }
            updateButtonStatus();
        } catch (Exception e) {
            handlingException(e);
        }
    }

    private void clickCompleteBtn() {
        btnComplete.click();
    }

    private void clearAll() {
        presenter.setPacking(false);
        scheduleTab.refresh();
        packingTabContent.clear();
        tfBarcode.setValue("");
    }

    @Override
    public String getDisplayValue() {
        return "WorkStation.Packingstation.Packing";
    }

    @Override
    public void enter() {
        try {
            if (!isPreview()) {
                packingTabContent.clear();
                clearAll();
                updateButtonStatus();
                scheduleTab.refresh();
            } else {
                btnSeal.setEnabled(false);
                btnPrint.setEnabled(false);
                btnComplete.setEnabled(false);
                tfBarcode.setEnabled(false);
            }
        } catch (Exception e) {
            handlingException(e);
        }
    }

    @Override
    public void activateParameters() {
        btnPrint.setVisible(isPrintNeeded);

        packingTabContent.setPackageDefinition();
        // scheduleTab.setPackageDefinition();

        if (Strings.isNullOrEmpty(packagePrinter)) {
            NotificationUtils.notificationError(
                I18NUtility.getValue("WorkStation.Packing.PrinterNotSet", "Package printer is not set!"));
        }
        packingTabContent.setPreview(isPreview());

    }

    @Override
    public BasePresenter<?> getPresenter() {
        return presenter;
    }

}

如果需要自定义左上角的标题和图片,可以使用以下两个方法:

getStageUI().getHeader().setImagePath("imagePath");
getStageUI().getHeader().setCaption("yourCaption");

2.3. 多人模式

2.3.1. 使用场景

存在这样的使用场景:在同一个工位中会有多人作业的情况,现实情况是在装配一个大型设备的时候,因为器件比较大所以 集中在某一个工位进行作业,领料方式一次性领取该工序所需的物料,和以往不同的是作业的人员为多个,并且需要实现以下功能:
1、该工位允许多名操作员同时登录。 2、记录工作时不同时间段不同的操作员自己相应的作业信息。

2.3.2. 具体实现

基于自定义表的多人模式将在以后版本中实现。

2.3.3. 具体应用范例

创建工作站时选择单人模式或多人模式,

单人模式: 过站逻辑和之前一样;

多人模式:进入工作中心,选择的工位为多人模式,header会出现一个“多人模式”的按钮,下拉点击添加入组,输入用户名和密码加入临时组。

如过允许在此工位上进行模式的切换,模式切换示例如下:

(1)单人模式—>单人模式:正常进入;

(2)单人模式—>多人模式:正常进入;

(3)多人模式—>多人模式:如果在该站没有创建临时组,直接进入;如果创建临时组,工位的切换会导致临时组被销毁,所以会跳出弹框,询问是否确定切换;

(4)多人模式—>单人模式:如果在该站没有创建临时组,直接进入;如果创建临时组,工位的切换会导致临时组被销毁,所以会跳出弹框,询问是否确定切换;

点击加入临时组中,也只有在该工作站分配了用户的有权限加入;

2.4. 数据字典配置

2.4.1. 在任意项目的pom文件下添加如下plugin

    <plugin>
    <groupId>com.ags.lumosframework</groupId>
    <artifactId>lumos-chm-tool</artifactId>
    <version>5.5.0-SNAPSHOT</version>
    <configuration>
        <databaseDriver>com.mysql.jdbc.Driver</databaseDriver> (1)
        <databaseUrl>
            <![CDATA[jdbc:mysql://119.119.118.108:3306/mes_444_2?useUnicode=true&characterEncoding=utf-8]]></databaseUrl>
        <databaseUser>root</databaseUser>
        <databasePassword>123456</databasePassword>
        <packagePaths>
            <packagePath>com/ags/lumosframework/sdk/entity</packagePath> (2)
            <packagePath>com/ags/lumosframework/device/sdk/entity</packagePath>
            <packagePath>com/ags/lumosframework/stage/api/entity</packagePath>
            <packagePath>com/ags/lumosframework/statemachine/sdk/entity</packagePath>
            <packagePath>com/ags/mes/server/api/entity</packagePath>
        </packagePaths>
    </configuration>
    <dependencies>
        <dependency> (3)
            <groupId>com.ags.mes</groupId>
            <artifactId>mes-core-api</artifactId>
            <version>4.4.4-SNAPSHOT</version>
        </dependency>
    </dependencies>
</plugin>
1 如果数据字典需要配置自定义表数据,需要连接的数据库信息(可选)
2 表示需要分析的entity的包路径名称(选择需要添加的项目的包路径名称)
3 表示需要分析的entity类所在的项目,需要添加对应依赖

2.4.2. 在该pom下运行该插件

Datadictionary01
或者使用maven命令
mvn lumos-chm-tool:chm

2.4.3. 在target目录下生成一个DataDictionaryItem目录,里面的就是生成的数据字典,可以直接访问(需要下载jquery.min.js 放在DataDictionaryItem目录里)

Datadictionary02

3. 代码生成工具

通过简单的页面配置迅速生成符合平台框架的代码.

3.1. 使用说明

codegenerator

1、项目,对应代码中的概念即一个工程

2、类,对应代码中的概念即一个Entity对象

3、字段和索引,对应代码中的概念即一个Entity对象的一个属性和索引

新建项目
codegenerator1

项目名决定了生成的项目的名称

表名前缀决定了数据库中表的前缀名

新增按钮右侧分别为修改项目,删除项目和刷新项目,功能不再赘述

输入信息后点击确认按钮即完成创建,在页面左侧项目列表里即可查看新建的项目

新建Entity
codegenerator2

前三项为国际化信息

继承类型分BuildtimeBaseEntity,RuntimeBaseEntity,BaseEntity以及Entity

所属模块决定了权限选择中该类所处的位置

是否为关系表决定了是否生成该类的前段页面

是否为逻辑删除决定了在删除的时候执行的是逻辑删除还是物理删除

是否支持页面定制化决定了该类的字段是否支持页面设置

新增按钮右侧分别为修改类,删除类和刷新类,功能不再赘述

输入信息后点击确认按钮即完成创建,在页面右上方类列表中即可查看新增的Entity

新增Column
codegenerator3

名称为字段名,使用驼峰写法

名称为国际化信息

类型为字段类型,使用String类型需要指定字段长度

平台不建议使用lombok,需要生成getter setter方法需要勾选最后一个选项

新增按钮右侧分别为修改字段,删除字段和刷新字段,功能不再赘述

输入信息后点击确认按钮即完成创建,在页面右下方Column列表中即可查看新增的Column

新增索引
codegenerator4

可以添加多个字段,组成组合索引

可以通过上下键来确定字段优先级

新增按钮右侧分别为修改索引,删除索引,功能不再赘述

生成代码压缩包

点击下载模板按钮,即可在网页上下载到根据页面配置得到的生成后代码的压缩包

codegenerator5

代码生成工具网页版公共地址:http://119.119.118.107:7381/CodeGenerator

4. 设备

设备模块是平台提供的用于设备管理及设备连接的功能模块,其包含的功能如下:

  • 设备类别及设备管理

  • 打印模板及其参数管理

  • 设备连接与交互

设备模块既可用作设备资产管理模块也可用作设备交互模块,作为设备资产管理模块用户可在页面对设备及相关数据进行管理,作为设备交互模块用户可以结合lumos自带设备连接平台或第三方IOT平台与设备进行交互。

设备模块的设备连接与交互功能原理如下(以打印为例):

device connect

在设备交互方面,目前只提供了打印机的连接与使用,在具体项目中,各企业、工厂使用的设备在类别、型号及生产厂家等方面都存在差异,而这些差异对设备交互的具体实现都有影响,基于这些原因,该模块在设备交互功能上提供了有力的扩展支持。

接下来本章节就从以下三个方面介绍设备模块的使用和扩展:

  • 集成与使用

  • 打印功能的使用及扩展

  • 设备交互的扩展

4.1. 集成与使用

4.1.1. 平台依赖

  1. 开发集成环境(Eclipse,IDEA)

  2. Maven3.6

  3. Lumos 3.0 及以上

4.1.2. 如何集成

如果在具体项目中需要使用设备模块,那么就要引入相应的Maven依赖,具体如下:

  • 在项目中引入设备模块的API,添加 lumos-device-impl 的依赖即可:

    <dependency>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-device-impl</artifactId>
    </dependency>

    如果项目结构类似平台即分别有sdk及impl项目,则在sdk项目中引入 lumos-device-sdk

    <dependency>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-device-sdk</artifactId>
    </dependency>
  • 在项目中引入设备模块的管理页面

    1. 首先需要添加 lumos-device-vaadin 的依赖:

      <dependency>
          <groupId>com.ags.lumosframework</groupId>
          <artifactId>lumos-device-vaadin</artifactId>
      </dependency>
    2. 然后引入页面:(设备模块提供的管理页面都需要使用wrapper的形式,显式地添加到具体项目中。)

      以设备管理页面为例:

      @Secured(DevicePermissionConstants.EQUIPMENT)
      @Menu(groupName = MesConstants.MENU_GROUP_Equipment, caption = "equipment", captionI18NKey = "device.equipment",
          iconPath = "images/icon/equipment.png", order = 1)
      @SpringView(name = "equipment", ui = ConfigurationUI.class)
      public class EquipmentViewWrapper extends EquipmentView {
      
          private static final long serialVersionUID = 3339287935391912314L;
      
      }

4.1.3. 如何使用

首先,本章所说的使用是API层面的使用,而非如何在页面上操作设备管理功能,如果想了解设备模块详细的功能及其操作可以查看相应的用户使用文档。

API的使用大致可分为两个部分,一是设备交互API的使用,二是设备建模数据管理相关API的使用。

  • 设备交互API的使用

    设备交互就是通过调用API操作或监控具体的设备,比如操作打印机打印标签、读取电子秤的称重数据。设备模块提供了 DeviceManager 类,通过调用 createDevice() 方法获取设备做后续操作,以读取电子秤的数据为例:

    @SpringComponent
    @Scope("prototype")
    public class WeightTab extends BaseComponent implements IWeightTab {
    
        private Equipment equ = null;
    
        private IScale createScale;
    
        @Override
        public void refresh(WIP<?> wip, RouteStep step) {
            equ = presenter.getEquipment(equipmentClass);
            if (equ != null) {
                createScale = DeviceManager.createDevice(equ.getEquipClass().getEquipClassName(),
                    equ.getDataSource(), equ.getIdentity());
                createScale.addDataReachedListener(WeightTab.this,
                    new DataReachedListener<ScaleData>() {
                        @Override
                        public void dataReached(ScaleData data) {
                            // Do something
                        }
                    });
            }
        }
    }

    其中 IScale 是电子秤的抽象接口:

    IScale.java
    public interface IScale extends DataReacheable<ScaleData>, IDevice {
    
    }
    DataReacheable.java
    public interface DataReacheable<DATA> {
    
        void addDataReachedListener(Object owner, DataReachedListener<DATA> dataReachedListener);
    
        void removeDataReachedListener(Object owner);
    
    }
  • 设备建模数据管理相关API的使用

    设备建模数据管理相关API的详细介绍可以查看相应API文档,需要注意的是,如何在调用设备交互API时获取使用到的设备对象,MES中是使用绑定的方式,比如在工作站中绑定设备,需要使用设备时根据绑定关系获取设备对象,当然也可以在某一个配置项中指定某个场景具体使用哪个设备。

4.2. 打印功能的使用及扩展

在大部分项目中打印都是一个必要功能,为此,设备模块提供了打印功能并且提供了一定的扩展能力。

4.2.1. 使用

在引入设备模块的依赖后,可以通过上一小节中的方法(即调用 DeviceManager 类的 createDevice() 方法)实现打印功能,鉴于打印功能的通过性,设备模块对打印进行了封装,可以通过简单的API调用实现打印功能,具体打印接口如下:

public interface IPrinterService extends IBaseService {

    /**
     * 统一的打印机调用接口,可以支持工单,批次,产品等的打印
     *
     * @param deviceId          打印机ID
     * @param obj               需要打印的对象(提供打印数据的对象)
     * @param printerTemplateId 打印模板ID
     */
    void print(long deviceId, Object obj, long printerTemplateId);

    /**
     * 统一的打印机调用接口,可以支持工单,批次,产品等的打印
     *
     * @param deviceId          打印机ID
     * @param objs              需要打印的对象(提供打印数据的对象)
     * @param printerTemplateId 打印模板ID
     */
    void print(long deviceId, List<?> objs, long printerTemplateId);

    /**
     * 通用的打印接口
     *
     * @param deviceId          打印机ID
     * @param printerTemplateId 打印模板ID
     * @param datas             打印数据
     */
    void print(long deviceId, long printerTemplateId, List<Map<String, String>> datas);
}

Note: 参数obj虽然为Object类型, 但如果不是ObjectBaseImpl的子类, 将没有打印日志。

以工单打印为例:

@Component
@UIScope
public class OrderManagementViewPresenter extends BasePresenter<IBaseView> implements IAssignSequence<WorkOrder> {
    @Inject
    private IPrinterService printerService;

    public void printOrder(PrintTemplate print, WorkOrder order, long deviceId) {
        printerService.print(deviceId, order, print.getId());
    }
}

4.2.2. 扩展

设备模块提供的打印功能需要配置打印模板及其参数,而设备模块本身并没有模板参数定义(MES提供了一系列的模板参数定义),要根据具体项目需求进行扩展.
  1. IPrintTemplateUsage : 扩展【打印模板】中【模板用途】

  2. PrinterParameterType : 扩展【打印模板 - 打印机参数】中【数据来源】

  3. IPrintTemplateParameter : 扩展【打印模板 - 打印机参数】中【数据来源】对应的参数

以MES中部分模板参数的定义为例,只用部分核心代码举例:

/**
* 模板用途: Unit, OrderItem...,
* 模板用途name必须全局唯一
*/

public enum PrintTemplateUsageType implements IPrintTemplateUsage {      (1)

    Unit("Unit"){
        @Override
        public List<IPrinterParameterType> getParameterTypes() {         (2)
            List<IPrinterParameterType> parameterTypes = new ArrayList<>();
            parameterTypes.add(PrinterParameterType.UNIT_INHERENT);
            parameterTypes.add(PrinterParameterType.DATA_COLLECTION);
            return parameterTypes;
        }
    },
    OrderItem("OrderItem"){
        @Override
        public List<IPrinterParameterType> getParameterTypes() {
            List<IPrinterParameterType> parameterTypes = new ArrayList<>();
            parameterTypes.add(PrinterParameterType.ORDERITEM_INHERENT);
            return parameterTypes;
        }
    };


    private String name;

    PrintTemplateUsageType(String name) {
        this.name = name;
    }
}
/**
* 每个模板用途都有对应的数据来源类型:getTemplateParameters()
*/
public enum PrinterParameterType implements IPrinterParameterType {

    /**
     *固有属性
     */
    UNIT_INHERENT("UnitInherent", "Common.Inherent", PrintParameterDisplayType.List){    (3)
        @Override
        public List<IPrintTemplateParameter> getTemplateParameters() {                    (4)
            List<IPrintTemplateParameter> parameters = new ArrayList<>();
            parameters.add(MesPrintParameterEnum.ORDER_NUMBER);
            parameters.add(MesPrintParameterEnum.ORDER_ITEM_NUMBER);
            return parameters;
        }
    },
    ORDERITEM_INHERENT(" OrderItemInherent", "Common.Inherent", PrintParameterDisplayType.List){
        @Override
        public List<IPrintTemplateParameter> getTemplateParameters() {
            List<IPrintTemplateParameter> parameters = new ArrayList<>();
            parameters.add(MesPrintParameterEnum.ORDER_ITEM_NUMBER);
            return parameters;
        }
    },
     /**
     *数据采集
     */
    DATA_COLLECTION("DataCollection", "Common.Data.Collection", PrintParameterDisplayType.Text){   (5)
        @Override
        public IPrintTemplateParameter getParameterReader(String paramName) {           (6)
            return MesPrintParameterEnum.DATA_COLLECTION;
        }
    };
    private String name;
    private String nameKey;
    /**
     *
    */
    private PrintParameterDisplayType displayType;
    PrinterParameterType(String name, String nameKey, PrintParameterDisplayType displayType) {
        this.name = name;
        this.nameKey = nameKey;
        this.displayType = displayType;
    }
}

public enum MesPrintParameterEnum implements IPrintTemplateParameter {

    /**
     * 工单编号
     */
    ORDER_NUMBER("OrderNumber", "", (obj, parameters) -> {
        if (obj instanceof WIP) {
            return ((WIP)obj).getWorkOrderNumber();
        }
        if (obj instanceof WorkOrder) {
            return ((WorkOrder)obj).getOrderNumber();
        }
        return "";
    }),
    /**
     * 工单项编号
     */
    ORDER_ITEM_NUMBER("OrderItemNumber", "", (obj, parameters) -> {
        if (obj instanceof WIP) {
            return ((WIP)obj).getWorkOrderItemNumber();
        }
        if (obj instanceof WorkOrderItem) {
            return ((WorkOrderItem)obj).getOrderItemName();
        }
        return "";
    }),
      /**
     * 数据采集
     */
    DATA_COLLECTION("DataCollection", "", (obj, parameters) -> {
        Map<String, String> result = new HashMap<>();
        if (obj instanceof WIP) {
            WIP<?> wip = (WIP<?>)obj;
            IDataAcquisitionResultService service = BeanManager.getService(IDataAcquisitionResultService.class);
            List<String> strParameters = new ArrayList<>();
            for (Object parameter : parameters) {
                strParameters.add((String)parameter);
            }
            List<DataAcquisitionResult> dataAcquisitionResults =
                service.listByName(wip.getObjectType(), wip.getSerialNumber(), strParameters);
            for (DataAcquisitionResult dataAcquisitionResult : dataAcquisitionResults) {
                result.put(dataAcquisitionResult.getName(), dataAcquisitionResult.getResultValue());
            }
        }
        return result;
    });

    private String name;
    private String nameKey;
    private Reader reader;
    MesPrintParameterEnum(String name, String nameKey, Reader reader) {
        this.name = name;
        this.nameKey = nameKey;
        this.reader = reader;
    }
}
1 模板用途可以选项:Unit, OrderItem。
2 模板用途Unit, 对应的数据来源有:UNIT_INHERENT DATA_COLLECTION。
3 数据来源UNIT_INHERENT的打印参数是个List(PrintParameterDisplayType.List),对应的列表值于【4】里面获取。
4 数据来源UNIT_INHERENT的打印参数集合, 默认返回Null, 当为PrintParameterDisplayType.List时,需要重写,当为PrintParameterDisplayType.Text时,此集合用不到, 不需要重写。
5 数据来源DATA_COLLECTION的打印参数是Text类型(PrintParameterDisplayType.Text),
6 当在IPrinterService.print()打印模板时解析参数对应的值, 默认根据【paramName】从【4】中找到对应的打印参数枚举类,并且调用read()方法获取 当为PrintParameterDisplayType.Text时,由于集合为空, 需要重写, 否则不需要重写,除非自己想重定义。

在对应项目的spring.factories文件中加入:

com.ags.lumosframework.device.sdk.base.IPrintTemplateParameter=\
com.ags.mes.server.api.enums.MesPrintParameterEnum
com.ags.lumosframework.device.sdk.base.IPrintTemplateUsage=\
com.ags.mes.server.api.enums.PrintTemplateUsageType
com.ags.lumosframework.device.sdk.base.IPrinterParameterType=\
com.ags.mes.server.api.enums.PrinterParameterType

4.3. 设备交互的扩展

上一小节介绍了打印机的使用及扩展,但大多数项目可能不单单有打印机这一种设备,这个时候就需要在设备模块的基础上进行扩展以支持更多设备的交互。 正如前文所述,设备模块设备交互功能是通过主服务端与设备连接平台进行交互,然后连接平台再与设备进行交互这种方式实现的, 所以对该功能进行扩展可分为服务端的扩展及连接平台端的扩展,当然,如果在实际项目使用的是第三方的IOT平台进行设备交互,那么只需对服务端进行扩展即可。 接下来就从这两个方面来介绍设备交互的扩展。

4.3.1. 服务端

服务端的扩展又可分为可连接平台的扩展及可连接设备的扩展,如果使用第三方的IOT平台那么就需要扩展可连接平台,如果是使用设备模块提供的设备连接平台则只需扩展可连接设备。

  • 可连接平台的扩展

    扩展可连接平台只需要实现 IDriverDef 接口即可

    以设备模块提供的DeviceAgent连接平台为例:

    public enum LumosEquipmentDriverDef implements IDriverDef {
    
        /**
         * 平台提供的连接平台
         */
        DEVICE_AGENT("DeviceAgent", "device.platform.deviceagent",new IParameterDef[] {});
    
        private String name;
    
        private String nameKey;
    
        private IParameterDef[] parameters;
    
        LumosEquipmentDriverDef(String name, String nameKey, IParameterDef[] parameters) {
            this.name = name;
            this.nameKey = nameKey;
            this.parameters = parameters;
        }
    
        @Override
        public String getName() {
            return name;
        }
    
        @Override
        public String getNameKey() {
            return nameKey;
        }
    
        @Override
        public List<IParameterDef> getParameterDefs() {
            return Arrays.asList(parameters);
        }
    
    }
  • 可连接设备的扩展

    • 添加设备的抽象接口

      继承 AbstractDevice 类并实现 IDevice 接口:

      示例:

      1. 定义打印机接口 IScanner

        public interface IScanner extends IDevice, DataReacheable<String> {
        
            String scan();
        
            void autoScan(int scanInterval);
        
        }
      2. 添加实现类 Scanner

        public class Scanner extends DataReacheableSupport<String> implements IScanner {
        
            public Scanner(DeviceInfo deviceInfo) {
                super(deviceInfo);
            }
        
            @Override
            public String scan() {
                DeviceKey deviceKey = getDeviceInfo().getDeviceKey();
                return (String)getDeviceMessageTransportHub().sendAndReceive(deviceKey, "SCAN");
            }
        
            @Override
            public void autoScan(int scanInterval){
                DeviceKey deviceKey = getDeviceInfo().getDeviceKey();
                getDeviceMessageTransportHub().send(deviceKey, "AUTO_SCAN");
            }
        }
        设备抽象的实现类必须提供以 DeviceInfo 为参数的构造函数。
    • 添加设备类别

      首先实现 IConnectableEquipmentClassDef 接口:

      示例:

      public enum DemoConnectableEquipmentClassDef implements IConnectableEquipmentClassDef {
      
          SCANNER("SCANNER", Scanner.class);
      
          private String name;
          private Class<? extends IDevice> deviceClass;
      
          LumosConnectableEquipmentClassDef(String name, Class<? extends IDevice> deviceClass) {
              this.name = name;
              this.deviceClass = deviceClass;
          }
      
          @Override
          public String getName() {
              return name;
          }
      
          @Override
          public Class<? extends IDevice> getDeviceClass() {
              return deviceClass;
          }
      
      }

      在对应项目的spring.factories文件中指定实现类,让Spring能够扫描到该实现类,多个实现类用逗号分隔。

      com.ags.mes.device.connector.core.device.IConnectableEquipmentClassDef=\
      com.ags.mes.device.connector.core.device.DeviceConnectableEquipmentClassDef

      然后需要在LiquiBase脚本中添加设备类别,将其保存到数据库中以便后续保存设备类别参数值。

      <insert tableName="SYS_EQUIPMENT_CLASS">
          <column name="EQUIP_CLASS_NAME" value="SCANNER" />
          <column name="PARAMETER" value="" />
          <column name="SYSTEM_DEFINED" valueBoolean="true" />
      
          <column name="ID" valueNumeric="1" />
          <column name="COMPANY_ID" valueNumeric="-1" />
          <column name="CREATE_TIME" valueDate="2019-06-01" />
          <column name="CREATE_USER_ID" valueNumeric="1" />
          <column name="CREATE_USER_NAME" value="admin" />
          <column name="CREATE_USER_FULL_NAME" value="admin" />
          <column name="DTS_CREATION_BID" valueNumeric="-1" />
          <column name="LM_TIME" valueDate="2019-06-01" />
          <column name="LM_USER_ID" valueNumeric="-1" />
          <column name="LM_USER_NAME" value="admin" />
          <column name="LM_USER_FULL_NAME" value="admin" />
          <column name="DTS_MODIFIED_BID" valueNumeric="-1" />
          <column name="DELETE_USER_ID" valueNumeric="-1" />
          <column name="DELETED" valueBoolean="false" />
      </insert>
      • 注意其中 EQUIP_CLASS_NAME 字段必须与前面定义的设备类别名称一致

      • 注意修改Id以免重复

4.3.2. 连接端(Device Agent)

如果使用设备模块自带的设备连接平台,在服务端进行扩展之后,相应的,也需要对连接端进行扩展以完善设备交互的功能。

  • 搭建DeviceAgentDemo项目

    由于DeviceAgent是一个独立的项目,对其进行扩展需要搭建一个依赖于DeviceAgent的项目。

    创建Maven项目device-agent-demo,pom如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>com.ags.lumosframework</groupId>
            <artifactId>lumos-device-agent</artifactId>
            <version>3.2.0</version>
        </parent>
    
        <artifactId>device-agent-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <dependencies>
            <dependency>
                <groupId>com.ags.lumosframework</groupId>
                <artifactId>lumos-device-agent</artifactId>
                <version>3.2.0</version>
            </dependency>
        </dependencies>
    </project>
    device agent demo

    配置文件:

    spring.application.name=Jasper-Device-Agent
    #mes.licence.dir=${user.home}
    server.servlet.context-path=/device
    server.port=8083
    mes.core.url=http://119.119.118.23:8080/Jasper
    device.internal.client.enabled=true
    
    spring.artemis.mode=NATIVE
    spring.artemis.host=localhost
    spring.artemis.port=61616
    spring.artemis.user=scott
    spring.artemis.password=admin

    启动项目,访问 localhost:8083/device-demo 进入客户端管理页:

    device agent demo home
  • 扩展

    与服务端对应,连接端也需要添加设备类实现 IDeviceDef 接口:

    示例:

    public enum DemoDeviceDef implements IDeviceDef {
    
        SCANNER("SCANNER", "device.demo.devicedef.scanner", ScannerMonitor.class,
            new IDeviceParameterDef[] {DemoDeviceParameterDef.COM});
    
        private String typeName;
        private String i18nNameKey;
        private Class<? extends IDeviceMonitor> monitorClass;
        private List<IDeviceParameterDef> parameterDefs;
    
        DemoDeviceDef(String typeName, String i18nNameKey, Class<? extends IDeviceMonitor> monitorClass,
            IDeviceParameterDef[] parameterDefs) {
            this.typeName = typeName;
            this.i18nNameKey = i18nNameKey;
            this.monitorClass = monitorClass;
            this.parameterDefs = Arrays.asList(parameterDefs);
        }
    
        @Override
        public String getTypeName() {
            return typeName;
        }
    
        @Override
        public String getI18NName() {
            return i18nNameKey;
        }
    
        @Override
        public Class<? extends IDeviceMonitor> getMonitorClass() {
            return monitorClass;
        }
    
        @Override
        public List<IDeviceParameterDef> getParameters() {
            return parameterDefs;
        }
    }
    • 注意,typeName必须与服务端定义的保持一致。

    • 必须在项目的META-INF/spring.factories文件中加入相应配置以便其受Spring容器管理

      com.ags.lumosframework.device.agent.base.IDeviceDef=\
      com.ags.lumosframework.device.demo.DemoDeviceDef
    • 其中DeviceMonitor是设备的处理类,控制设备的启停及接受到命令时的动作(子类的scope需是prototype)。

      @Component
      @Scope("prototype")
      public class ScannerMonitor extends DeviceMonitor {
      
          public ScannerMonitor(DeviceConfiguration deviceConfiguration) {
              super(deviceConfiguration);
          }
      
          @Override
          protected void doStartProcess() {
              // 启动Monitor时所需的操作
              // deviceConfiguration是可以直接使用的,该对象中有
              System.out.println(deviceConfiguration.getType() + "-" + deviceConfiguration.getIdentity() + "-"
                  + deviceConfiguration.getParameters());
          }
      
          @Override
          protected void doStopProcess() {
              // 停止Monitor时所需的操作
          }
      
          // 收到命令时执行的方法
          @Override
          public Object commandReceived(Message<?> message) {
              String command = (String)message.getPayload();
              if("SCAN".equals(command)){
                  // 扫描并返回值
                  return "";
              }else if("AUTO_SCAN".equals(command)){
                  // 执行自动扫描
              }
              return null;
          }
      }
    • 参数就是在DeviceAgent端能够配置的参数,比如COM口。

      参数的定义可以实现 IDeviceParameterDef 接口:

      示例:

      public enum DemoDeviceParameterDef implements IDeviceParameterDef {
          COM("COM", "COM", "device.demo.deviceparameter.com");
      
          private String key;
          private String name;
          private String nameKey;
      
          DemoDeviceParameterDef(String key, String name, String nameKey) {
              this.key = key;
              this.name = name;
              this.nameKey = nameKey;
          }
      
          @Override
          public String getKey() {
              return key;
          }
      
          @Override
          public String getName() {
              return name;
          }
      
          @Override
          public String getNameI18NKey() {
              return nameKey;
          }
      
      }

      如果参数是一个限定范围的可选项,则可实现IDeviceParameterDef的 getParameterValues() 方法。可选值的定义只需实现 IParameterValue 接口。

5. 异常报警管理功能扩展

异常报警管理功能主要包括两部分:一是生产异常监控,即针对生产过程中产生的异常问题,通过人工或自动的方式记录下来,二是将异常信息按系统预置的方式自动向分层管理领导发送通知,以反馈到相关人员进而快速处理异常问题,最后并记录异常处理的过程。

5.1. 环境安装

5.1.1. 平台依赖

  1. 开发集成环境(Eclipse,IDEA)

  2. Maven3.6

  3. Lumos

5.2. 定制项目中如何引用

要在项目中使用异常管理模块,只需要引入对应的jar包即可。

定制项目sdk项目依赖引入

将如下依赖加入定制项目的SDK(也就是接口定义项目)中。

<dependency>
    <artifactId>exception-management-sdk</artifactId>
    <groupId>com.ags.lumosframework</groupId>
    <version>3.2.0</version>
</dependency>
定制项目后端依赖引入

将如下依赖加入定制项目的后端服务实现中。

<dependency>
    <groupId>com.ags.lumosframework</groupId>
    <artifactId>exception-management-impl</artifactId>
    <version>3.2.0</version>
</dependency>
定制项目前端依赖引入

将如下依赖加入定制项目的后端服务实现中。

<dependency>
    <groupId>com.ags.lumosframework</groupId>
    <artifactId>exception-management-web</artifactId>
    <version>3.2.0</version>
</dependency>
如果定制项目的项目接口没有这么详细的划分,比如说所有的项目实现都在一个项目中,那么将上述依赖全局加入这个项目的pom文件即可。
异常管理界面进入

一旦加入上述的依赖,重新启动程序后,你将会看到如下界面

excp

5.3. 异常管理功能设计说明

异常管理包括异常代码类型的定义、异常代码的定义、异常推送规则的定义及配置、异常记录、异常处理、异常报警触发等五部分。

5.3.1. 异常代码类型

即异常分类,包括编码、名称、描述等字段,其中编码和名称必须唯一。

excp codetype

5.3.2. 异常代码

即某个异常分类下有哪些具体的异常,包括异常代码类型、编码、名称、描述等字段,其中编码和名称必须唯一。

excp code

5.3.3. 异常推送规则

异常规则的定义

包括名称、代码、推送等级、推送方式、推送人员、响应时间、处理时间等定义。

excp rule
异常推送任务

平台提供了定时任务,去监控异常记录的产生,当异常发生时,并且此异常的响应或处理时间超出系统的预设时间,将按系统预置方式自动向分层管理领导发送通知(包括站内信、邮件等)。

推送规则如下:

当平台定时任务监听到有未关闭的异常记录时,首先去找此异常记录当前级别所对应的异常推送规则:

  • 如果异常还没有人响应:则判断【当前时间距上次推送时间】是否已超过了当前级别所定义的【响应时间】,若是则向下一级推送通知。

  • 如果异常已经有人响应:则判断【当前时间距上次推送时间】是否已超过了当前级别所定义的【处理时间】,若是则向下一级推送通知。

例如:

rule

CL000001分别定义了四个级别的异常规则,当此规则对应的异常记录产生时,系统会先找到级别最低的J1,如果实际响应时间超过了J1定义的响应时间(10分钟),则会推送通知给J2的处理人,以此类推,逐级推送,直到推送给了最高级别或者异常已被关闭,将不再推送。

5.3.4. 异常记录

异常记录的定义

异常记录的内容包括异常代码、产线、车间、是否停线、状态等,其产生的方式包括手动和自动两种。

excp record

平台提供了创建异常记录的接口,定义如下:

public interface IQaExceptionRecordService extends IBaseDomainObjectService<QaExceptionRecord> {

    /**
     * 根据报警规则获取报警记录(双方通过绑定的异常代码连接)
     *
     * @param excpRuleId
     *            异常规则ID
     * @return 异常记录列表
     */
    List<QaExceptionRecord> listByRuleId(Long excpRuleId);

    /**
     * 获取还没解决的异常记录(包括未开始的和处理中的)
     *
     * @return 异常记录列表
     */
    List<QaExceptionRecord> listByUnsolvedRecord();

    /**
     * 获取异常记录的实际响应时间和处理时间
     *
     * @param excpRecordId
     *            异常代码ID
     *
     * @return 返回值包含两个:第一个是实际响应时间,第二个是实际处理时间。
     */
    ZonedDateTime[] getHandleTimesByRecordId(long excpRecordId);

    /**
     * 根据异常代码生成一条异常记录
     *
     * @param excpCodeId
     *            异常代码ID
     */
    void createExcpRecord(long excpCodeId);

    /**
     * 根据异常代码生成一条异常记录,并指定其当前推送等级
     *
     * @param excpCodeId
     *            异常代码ID
     * @param currentPushLevel
     *            指定当前推送等级值
     */
    void createExcpRecord(long excpCodeId, int currentPushLevel, long userId);

}
异常处理

当异常发起后,系统按照异常推送规则向相关人员发送异常通知,相关人员收到通知后,处理问题并在系统中记录处理结果。 处理过程中也可以移交他人,直至完成处理。

excp handle

5.3.5. 异常报警触发

平台提供了触发条件的前后端接口,用于支持触发条件的定制,并提供了定时任务根据触发条件自动产生异常记录。

触发条件前端页面的扩展

用户可以自由设计前端页面,只需实现 IQaTriggerConditionsTab 接口,并加上 @Primary 注解即可。

IQaTriggerConditionsTab接口定义
public interface IQaTriggerConditionsTab {
    /**
     * 触发器选中时触发
     */
    void triggerSelected(QaTriggerDefinition trigger);

    /**
     * @return 返回触发条件的标题
     */
    String getTabCaption();
}

平台提供了默认实现( QaTriggerConditionsTab ),但仅作为参考示例并无实际意义,效果图如下:

excp add condition
触发条件对应的java对象及数据库表也可以自由定义,系统中默认的对象为 QaTriggerConditions ,对应的表为 SYS_QA_TRIGGER_CONDITIONS
触发条件后台接口的扩展

平台提供了触发条件的后台接口,此接口中 verify() 方法返回值即代表触发条件的最终结果,平台中的定时任务会去监听这些结果,并以此来判断是否要产生异常信息。

IExcpTriggerConditions接口定义
public interface IExcpTriggerConditions {
    /**
     * @return 返回执行监听的名称
     */
    String getName();

    /**
     * @return 返回触发条件对应的触发器
     */
    QaTriggerDefinitionEntity getExcpTrigger();

    /**
     * 触发条件的检验方法,返回最终的验证结果
     */
    boolean verify();

    /**
     * 创建异常记录
     */
    QaExceptionRecordEntity createExcpRecord();
}

6. LSH服务

LSH可以作为系统服务独立运行,主要用于数据监控,测试数据采集,性能分析等用途,另外可以通过RPC调用各个模块提供的RPC服务。 目前提供三种服务即:Standalone Service、定时任务和WebService服务。

6.1. 项目搭建

LSH本身不包含任何业务功能,如果在具体项目中,需要使用LSH,那么就需要先搭建基于 lumos-jsh-base 的项目,在此基础上进行扩展具体的业务逻辑。

6.1.1. Maven配置

创建Maven项目,pom依赖如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <artifactId>lumos-jsh-demo</artifactId>
    <parent>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-jsh</artifactId>
        <version>3.3.0-SNAPSHOT</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>com.ags.lumosframework</groupId>
            <artifactId>lumos-jsh-base</artifactId>
            <version>3.3.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>com.vaadin</groupId>
                <artifactId>vaadin-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>update-widgetset</goal>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

只要引入各模块的SDK就可以通过RPC的方式无缝的调用对应模块的API。

例如引入lumos-appbase-sdk,就可以调用lumos-appbase中提供的API

<dependency>
    <groupId>com.ags.lumosframework</groupId>
    <artifactId>lumos-appbase-sdk</artifactId>
</dependency>

由于LSH调用其他模块的API本质上是通过RPC调用,而平台是使用Dubbo来实现RPC调用的,所以在使用时需要加入对应的配置,并且,如果使用了Zookeeper作为服务注册中心,则需要加入对应的pom依赖:

# artemis相关配置
lumos.artemis.broker-url=tcp://localhost:61616?ha=false&reconnectAttempts=-1
lumos.artemis.user=admin
lumos.artemis.password=admin

# Dubbo相关配置
dubbo.application.id=${spring.application.name}
dubbo.protocol.name=dubbo
dubbo.protocol.port=-1
dubbo.consumer.timeout=10000000

# 如果使用了Zookeeper作为服务注册中心,加入以下配置
spring.cloud.zookeeper.connect-string=localhost:2181
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>

6.1.2. 配置文件

# 通用配置
server.servlet.context-path=/lsh
server.port=8082
spring.application.name=Lumos-Lsh-Service
spring.main.allow-bean-definition-overriding=true
file.size=10

# 集群环境下的配置
lumos.node.cluster-check-interval=5
lumos.node.heart_beat_interval=2
lumos.node.node-id=node8083
lumos.loadbalance.strategy=polling

# 配置数据库连接信息
#-----h2数据库连接配置-----
#spring.datasource.url=jdbc:h2:mem:test
#spring.datasource.driver-class-name=org.h2.Driver
#spring.datasource.username=root
#spring.datasource.password=123456

#-----SqlServer数据库连接配置-----
#spring.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver
#spring.datasource.url=jdbc:sqlserver://119.119.118.157:1433;DatabaseName=lisa-lsh;SelectMethod=cursor
#spring.datasource.username=sa
#spring.datasource.password=P@ssw0rd
#spring.jpa.database-platform=org.hibernate.dialect.SQLServer2008Dialect


#-----Oracle数据库连接配置-----
#spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
#spring.jpa.database-platform=org.hibernate.dialect.Oracle10gDialect
#spring.datasource.url=jdbc:oracle:thin:@(description=(address_list= (address=(host=119.119.118.150) (protocol=tcp)(port=1522))(load_balance=yes)(failover=yes))(connect_data=(service_name= pdb)))
#spring.datasource.username=JASPER_LSH_TRACY
#spring.datasource.password=123456

#-----MySQL数据库连接配置-----
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/jasperlsh
spring.datasource.username=root
spring.datasource.password=123456
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect

spring.jpa.hibernate.ddl-auto=update

# 定时任务相关配置
spring.quartz.properties.org.quartz.scheduler.instanceName=DefaultQuartzScheduler
spring.quartz.properties.org.quartz.scheduler.instanceId=instance_one
spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
spring.quartz.properties.org.quartz.threadPool.threadCount=25
spring.quartz.properties.org.quartz.threadPool.threadPriority=5
spring.quartz.properties.org.quartz.jobStore.misfireThreshold=60000
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
spring.quartz.properties.org.quartz.jobStore.useProperties=false
spring.quartz.properties.org.quartz.jobStore.tablePrefix=QRTZ_
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=20000
spring.quartz.job-store-type=jdbc
  • LSH支持集群部署,如果使用集群部署,那么在数据库配置中指定MySQL等独立数据库,如果是单节点部署则可以使用内存数据库,如示例中的H2数据库,需要注意的是,使用H2数据库的内存模式,在服务停止后数据将被清除,可以改变URL将数据持久化到硬盘文件中。

    1、如果多个节点指定同一数据库并且spring.application.name一致就视为同一集群
    2、可通过 lumos.node.cluster-check-interval 配置项指定集群环境中Service刷新间隔(单位:秒),默认为5s
    3、可通过 lumos.node.heart_beat_interval 配置项指定节点心跳间隔(单位:秒),默认为2s
    4、对于LSH中的StandaloneService支持均衡负载,可通过 lumos.loadbalance.strategy 配置项指定负载策略,目前只有轮询(polling)一种。
  • 另外,开启分布式事务的支持,请参考Mes开发文档(3. MES开发手册 3.1. 分布式事务服务器(txlcn)部署手册)

启动类加【@EnableDubbo】注释
@SpringBootApplication(scanBasePackages = "com.ags.lumos.jsh.demo")
@EnableDubbo
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

项目搭建完成启动后,登入账户默认为admin/admin, 登入后访问页面如:

lsh

6.2. 服务接口

LSH提供三种服务支持:定时任务服务、Standalone Service和webservice服务。在扩展完成后,可以在页面上管理对应的服务,控制服务的启停,修改定时任务的执行规则等。

6.2.1. 定时任务服务

LSH中的定时任务与平台提供的后台任务功能一致,可以根据需要指定其按一定时间间隔执行还是按Cron表达式执行。

继承 TimerServiceInstanceSupport 类定义任务以及它索要执行的逻辑:

@Component
public class DemoTimerService extends TimerServiceInstanceSupport {

    private static final long serialVersionUID = -1250061323706258570L;

    private static final String ID = "TIMER";

    @Override
    public String getId() {
        return ID;
    }

    @Override
    public void execute() {
        // 执行具体的业务逻辑
    }

    @Override
    public String getName() {
        return "Demo Timer";
    }

    @Override
    public String getDescription() {
        return "Description for Test";
    }

    @Override
    public String getNameKey() {
        return "lsh.timer.demo.name";
    }

    @Override
    public String getDescriptionKey() {
        return "lsh.timer.demo.description";
    }

    @Override
    public List<IParameterDef> getQuartzParameters() {
        return new ArrayList<>();
    }

}

其中核心的就是execute()方法,在任务的执行时,系统会调用该方法执行具体的业务逻辑。

6.2.2. Standalone Service

Standalone Service是LSH提供的一种支持Failover及均衡负载的单独服务。

示例:

@Component
@Scope("prototype")
public class DemoStandaloneService extends BaseServiceInstance implements IServiceInstance {

    private static final long serialVersionUID = 4090076956381696626L;

    @Autowired
    private IServiceInstanceParameterService serviceInstanceParameterService;

    @Override
    public String getName() {
        return "Standalone Service";
    }

    @Override
    public String getDescription() {
        return "this is a test standalone Service";
    }

    @Override
    public String getNameKey() {
        return "";
    }

    @Override
    public String getDescriptionKey() {
        return "";
    }

    @Override
    public void doStart() {
        System.out.println("Start");
    }

    @Override
    public void doStop() {
        System.out.println("Stop");
    }

    @Override
    public void doDestroy() {}
}

定义好以后可以在管理页面添加、启动、停止,启动时会调用doStart()方法,停止会调用doStop()方法,删除会调用doStop()及doDestroy()方法。

  • Standalone Service支持参数,添加参数需要先定义参数:

    public enum DemoStanaloneParameterDef implements IParameterDef {
    
        NAME("Name", "demo.name", ParameterType.String);
    
        private String name;
    
        private String nameI18NKey;
    
        private ParameterType parameterType;
    
        DemoStanaloneParameterDef(String name, String nameI18NKey, ParameterType parameterType) {
            this.name = name;
            this.nameI18NKey = nameI18NKey;
            this.parameterType = parameterType;
        }
    
        @Override
        public String getKey() {
            return name;
        }
    
        @Override
        public String getName() {
            return name;
        }
    
        @Override
        public String getNameI18NKey() {
            return nameI18NKey;
        }
    
        @Override
        public ParameterType getType() {
            return parameterType;
        }
    }

    然后在定义Service时实现 getParameterDefs() 方法,示例如下:

    @Component
    @Scope("prototype")
    public class DemoStandaloneService extends BaseServiceInstance implements IServiceInstance {
    
        private static final long serialVersionUID = 4090076956381696626L;
    
        // ...
    
        @Override
        public List<IParameterDef> getParameterDefs() {
            List<IParameterDef> parameterDefs = new ArrayList<>();
            parameterDefs.add(DemoStanaloneParameterDef.NAME);
            return parameterDefs;
        }
    
        @Override
        public boolean autoStart() {
            return true;
        }
    }

    效果如下:

    standaloneservice parameter

    可以在页面上编辑参数,在页面上配置完成则可以在逻辑代码中使用,示例如下:

    @Component
    @Scope("prototype")
    public class DemoStandaloneService extends BaseServiceInstance implements IServiceInstance {
    
        private static final long serialVersionUID = 4090076956381696626L;
    
        @Autowired
        private IServiceInstanceParameterService serviceInstanceParameterService;
    
        // ...
    
        @Override
        public void doStart() {
            List<ServiceInstanceParameterEntity> parameterEntities =
                serviceInstanceParameterService.listByServiceInstance(this);
            for (ServiceInstanceParameterEntity parameterEntity : parameterEntities) {
                System.out.println(parameterEntity.getParameterName() + ": " + parameterEntity.getParameterValue());
            }
        }
        // ...
    }
  • Standalone Service可以声明为不需要在页面添加,而是在系统启动时自动添加并启动。

    只需要实现 autoStart() 方法返回 true 即可,示例:

    @Component
    @Scope("prototype")
    public class DemoStandaloneService extends BaseServiceInstance implements IServiceInstance {
    
        private static final long serialVersionUID = 4090076956381696626L;
    
        // ...
    
        @Override
        public boolean autoStart() {
            return true;
        }
    }

6.2.3. webservice服务

要发布WebService服务首先需要定义一个具体业务的接口,然后定义其实现类继承 WebServiceInstanceSupport 类。

示例:

hello.java
public interface Hello {

    String hello(String hello);

}
DemoWebServiceService.java
@Component
@Scope("prototype")
public class DemoWebServiceService extends WebServiceInstanceSupport implements Hello {

    private static final long serialVersionUID = -4408373440344089066L;

    @Override
    public Class<?> getServiceInterface() {
        return Hello.class;
    }

    @Override
    public String hello(String hello) {
        return hello;
    }

    @Override
    public String getName() {
        return "Demo Webservice";
    }

    @Override
    public String getDescription() {
        return "This is a test web services";
    }

    @Override
    public String getNameKey() {
        return "lsh.web-service.demo.name";
    }

    @Override
    public String getDescriptionKey() {
        return "lsh.web-service.demo.description";
    }
}

扩展完成后在页面添加即可发布WebService接口,效果如下:

web service
Figure 5. 效果图
wsdl
Figure 6. 对应的WSDL

6.2.4. 菜单扩展

如果在具体项目需要在现有菜单的基础上再增加新的菜单新的页面,只需要新增一个 View 继承自 BaseView ,并且在View类上添加注解 @SpringView@Menu 即可。

示例:

@Menu(caption = "测试", captionI18NKey = "测试", iconPath = "images/icon/area.png", order = 1)
@SpringView(name = "demo", ui = JshUI.class)
public class DemoView extends BaseView {

    public TestView() {
        HorizontalLayout hlRoot = new HorizontalLayout();

        Button aa1 = new Button("测试");

        hlRoot.addComponent(aa1);

        this.setSizeFull();
        this.setCompositionRoot(hlRoot);
    }
}

效果如下:

extension menu
平台的两个菜单的排序编号(Order)为10和20,在扩展菜单时,可将自定义的菜单加入到所需的位置。示例中排序编号为1,所以展示在最前面。

7. 状态机

状态机是一个低侵入性的状态管理的功能模块,通过该模块可以集中的管理对象状态,便捷的设置状态变迁规则以及追踪对象状态的变迁记录。

7.1. 平台依赖

  1. 开发集成环境(Eclipse,IDEA)

  2. Maven3.6

  3. Lumos 3.0 及以上

7.2. 集成

如果在具体项目中需要使用状态机模块,那么就要引入相应的Maven依赖,具体如下:

  • 在项目中引入状态机模块的API,添加 lumos-statemachine-impl 的依赖即可:

    <dependency>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-statemachine-impl</artifactId>
    </dependency>

    如果项目结构类似平台即分别有sdk及impl项目,则在sdk项目中引入 lumos-statemachine-sdk

    <dependency>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-statemachine-sdk</artifactId>
    </dependency>
  • 在项目中引入状态机模块的管理页面,只需添加`lumos-statemachine-vaadin` 依赖即可。

    <dependency>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-statemachine-vaadin</artifactId>
    </dependency>

7.3. 使用

该模块提供的状态管理功能是低侵入性的,所以使用时不需要在具体的对象中增加额外的字段来保存状态相关信息,只需要在页面上创建所需状态机然后调用相应API即可,具体使用方法如下:

首先需要在页面上创建状态机、变迁规则及制定初始状态。

示例:

state machine
详细页面操作请参考用户手册。

在创建好状态机及其变迁规则后,通过使用状态机模块提供的API即可便捷的管控具体对象的状态变迁。

常用API如下:

public interface IStateMachineInstanceService extends IBaseDomainObjectService<StateMachineInstance> {

    /**
     * 创建指定对象的状态机并初始化
     *
     * @param stateMachineId
     *            状态机id
     * @param objectId
     *            对象id
     * @param objectType
     *            对象类型
     */
    StateMachineInstance createNewInstance(long stateMachineId, String objectType, long objectId);

    /**
     * 根据stateMachineId 和 objectType和 objectId查询状态机对象实例
     *
     * @param stateMachineId
     *            状态机id
     * @param objectType
     *            对象类型
     * @param objectId
     *            对象id
     * @return
     */
    StateMachineInstance getByStateMachineIdAndObjectTypeAndObjectId(long stateMachineId, String objectType, long objectId);

    /**
     * 根据状态机id更新状态机对象实例当前状态
     *
     * @param stateMachineId
     *            状态机id
     * @param objectType
     *            对象类型
     * @param objectId
     *            对象id
     * @param targetState
     *            目标状态
     */
    void transition(long stateMachineId, String objectType, long objectId, String targetState);

    /**
     * 根据状态机id更新状态机对象实例当前状态
     *
     * @param stateMachineId
     *            状态机id
     * @param objectId
     *            对象id
     * @param targetState
     *            目标状态
     */
    void transition(long stateMachineId, long objectId, String targetState);

    /**
     * 根据实例对象id和目标状态更新对象实例当前状态
     *
     * @param instanceId
     *            对象实例id
     *
     * @param targetState
     *            目标状态
     */
    void transition(long instanceId, String targetState);

}
可以在具体项目中定义状态机名称常量,方便查找使用的状态机。

使用示例:

(注意,这里以工单对象为例演示状态机的使用,实际上,工单对象的状态变更并不是使用状态机模块实现的。)

  1. 在创建好状态机后,定义常量:

    public class DemoSateConstants {
        // 状态机名称
        public static final String ORDER_STATE_MACHINE = "ORDER_STATE";
    
        // 自定义的状态
        public static final String STATE_CREATED = "CREATED";
        public static final String STATE_RELEASE = "RELEASE";
        public static final String STATE_FINISHED = "FINISHED";
    }
  2. 在新建的工单对象保存时创建一个状态机实例并初始化:

    @Service
    public class WorkOrderService implements IWorkOrderService {
    
        @Autowired
        private IStateMachineService stateMachineService;
    
        @Autowired
        private IStateMachineInstanceService stateMachineInstanceService;
    
        @Override
        public void saveAll(List<WorkOrder> workOrders){
            for(WorkOrder workOrder : workOrders) {
                boolean isNew = workOrder.getId()<1;
                workOrder = getHandler().save(workOrder);
                // 判断如果是新建的对象,则创建一个状态机实例并初始化
                // 这里需要注意:先保存workOrder确保其Id值已被生成
                if(isNew){
                    StateMachine stateMachine = stateMachineService.getByName(DemoSateConstants.ORDER_STATE_MACHINE);
                    if(stateMachine != null){
                        stateMachineInstanceService.createNewInstance(stateMachine.getId(), MesObjectType.WorkOrder.getName(), workOrder.getId());
                    }
                }
            }
        }
    
        // ...
    
    }
  3. 在发布时,调用 transition() 方法变更状态。

    @Service
    public class WorkOrderService implements IWorkOrderService {
    
        @Autowired
        private IStateMachineService stateMachineService;
    
        @Autowired
        private IStateMachineInstanceService stateMachineInstanceService;
    
        //...
    
        @Override
        public void release(WorkOrder workOrder){
            super.release(workOrder);
            // 在工单发布后变更状态
            StateMachine stateMachine = stateMachineService.getByName(DemoSateConstants.ORDER_STATE_MACHINE);
            if(stateMachine != null){
                stateMachineInstanceService.transition(stateMachine.getId(), MesObjectType.WorkOrder.getName(), workOrder.getId(), DemoSateConstants.STATE_RELEASE);
            }
        }
    
    }

在状态管理及变迁功能外,该模块还提供了具体对象状态变迁记录的查询功能。

常用接口如下:

public interface IStateMachineHistoryService extends IBaseDomainObjectService<StateMachineHistory> {

    /**
     * 根据对象实例id查询状态机对象实例状态变更历史记录
     *
     * @param stateMachineInstanceId
     *            对象实例id
     * @return
     */
    List<StateMachineHistory> listByStateMachineInstanceId(long stateMachineInstanceId);

    /**
     * 根据状态机id和对象类型和对象id查询状态变更历史记录
     *
     * @param stateMachineId
     *            状态机id
     * @param objectType
     *            对象类型
     * @param objectId
     *            对象id
     * @return
     */
    List<StateMachineHistory> listByStateMachineIdAndObjectTypeAndObjectId(long stateMachineId, String objectType, long objectId);

    /**
     * 根据状态机id和对象id查询状态变更历史记录
     *
     * @param stateMachineId
     *            状态机id
     * @param objectId
     *            对象id
     * @return
     */
    List<StateMachineHistory> listByStateMachineIdAndObjectId(long stateMachineId, long objectId);

}

8. 数据迁移

8.1. 准备工作

需要一个目标数据库(下文称作历史库),历史库的表结构必须与生产环境数据库表结构完全一致(可以通过dbscript生成表或者直接在目标数据库(空库)上启动一次app项目),需要迁移的项目必须使用 模块化方式启动(即使用zookeeper)

8.2. 数据迁移项目配置

数据迁移项目所有可配置项如下:

spring.application.name=lumos-migration
dubbo.application.id=${spring.application.name}
dubbo.protocol.name=dubbo
dubbo.protocol.port=20992
dubbo.consumer.timeout=1000000
server.port=8082
server.servlet.context-path=/lumos-migration
lumos.artemis.broker-url=tcp://119.119.118.161:61616?ha=false&reconnectAttempts=-1
lumos.artemis.user=admin
lumos.artemis.password=admin
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mes2?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai (1)
spring.datasource.username=root
spring.datasource.password=123456
lumos.data.transfer.cron-expression=0 0/1 * * * ?  (2)
lumos.data.transfer.days-offset=0  (3)
spring.cloud.zookeeper.connect-string=119.119.118.224:2181 (4)
1 历史库的连接信息
2 执行周期,使用cron表达式
3 执行偏移天数(使用负数表示transfer多少天之前的数据),例如:值为-7则代表迁移执行日期七天前的所有数据
4 zookeeper地址

8.3. 作用范围

迁移的数据包括所有的runtime对象(项目启动后创建的对象),表类型为可清除的自定义表,所有runtime对象的拓展属性,不包括buildtime对象(程序启动时,初始化的对象)

8.4. 使用

数据迁移项目启动可以使用wrapper的方式,启动时指定容器的hostname为宿主机IP。启动后无操作页面,程序在后台运行,到了执行时间会自己去迁移数据。迁移后,生产环境数据将会被删除。

附:MES中所有的runtime对象对应表如下: SYS_PD_EXEC_NODE、SYS_SHIFT_INSTANCE、SYS_STATUS_CURRENT、SYS_SEQUENCE_WORKORDER、SYS_PD_EXEC_STACK、SYS_WORKORDER_ITEM、SYS_WORKORDER、SYS_NOTE、SYS_THIRDMODULE_INFO 、SYS_STATUS_HISTORY、SYS_OBA_RESULT、SYS_PD_EXEC、SYS_PD_EXEC_NODE_EQUIPMENT、SYS_DATA_ACQUISITION_RESULT、SYS_PRINT_LOG、SYS_OBJECT_STATION_IMAGE 、SYS_CONSUMPTION_RESULT、SYS_PACKAGE_ITEM、SYS_TAG_LOG、SYS_UNIT、SYS_LOT

8.5. 代码方面

如果想要在代码中切换到历史库,需要设置requestInfo.isToUseHistoryDatasource(true),注意:使用时必须使用try-catch-finally的形式,之后将该属性再设置为false,并且不要在历史库 中进行写操作

9. SPC图表组件

新增SPC视图组件,专用于SPC数据展示,用于展示数据、点位、标准线等如下图。

..\images\spc\image 2024 07 23 11 02 05 571

9.1. 集成

在Vaadin View中直接使用LumosSpcChart类直接进行初始化渲染。或者直接在PageEditor设计页面的图表组件中拖拽,通过ViewModel进行赋值。

  • LumosSpcChart

    com.ags.meperframework.suites.appbase.vaadin.designer.originalcomponent.chart.LumosSpcChart
    • 通过构造方法进行初始化

    LumosSpcChart spc = new LumosSpcChart();
    • 通过设计页面拖拽后,通过Widget进行处理

    @Widget("spcChart")
    private LumosSpcChart spcChart;

9.2. 数据渲染

  • SpcChartViewModel

SPC图表渲染的数据模型,通过数据初始化当前的model设置SPC的图表渲染。

com.ags.meperframework.suites.appbase.vaadin.component.spcchart.SpcChartViewModel

通过LumosSpcChart组件的内置方法设置数据

SpcChartViewModel model = new SpcChartViewModel();
LumosSpcChart spc = new LumosSpcChart();
spc.initChart(model);

9.3. 数据模型

  • SpcChartViewModel

public class SpcChartViewModel {

    //SPC 图表标题
    private String title;

    //SPC Data
    private List<SpcChartData> datas;

    //SPC 标准线
    private List<SpcChartBaseLine> lines;

    //SPC 点位
    private List<SpcChartPointModel> points;

    //SPC 错误信息
    private SpcErrorMessage errorMessage;

    //SPC Y轴数据单位
    private String ySuffix = "";

}
  • title

SpcChartViewModel model = new SpcChartViewModel();
model.setTitle("MEPER SPC Chart");
LumosSpcChart spc = new LumosSpcChart();
spc.initChart(model);
..\images\spc\image 2024 07 23 11 41 48 521
  • List<SpcChartData>

SpcChartData定义SPCChart的Data模型

public class SpcChartData {

    //X轴数据
    private String[] xData;

    //Y轴数据
    private Double[] yData;

    //图例名称
    private String name;

    //Data线颜色
    private String lineColor = SpcColor.DARK_BLUE.getHex();

    //Data点颜色
    private String markerColor = SpcColor.DARK_BLUE.getHex();

    //Data线粗细
    private int lineWidth = 2;

    //Data点大小
    private int markerWidth = 8;

    //是否直接显示Data点位数据
    private boolean showText;

    //点位数据显示类型
    private String showType = SpcChartDataShowType.TEXT.getType();

    //是否显示图例
    private boolean showlegend = true;

    //特殊点位配置
    private List<SpcChartDataPoint> points;

}
  • SpcChartDataPoint

特殊点位数据模型定义

public class SpcChartDataPoint {

    //X轴点位
    private String xData;

    //点位颜色
    private String markerColor = SpcColor.RED.getHex();

    //点位图例信息
    private String markerText = StringUtils.EMPTY;

}
  • Data初始化渲染图表

SpcChartViewModel model = new SpcChartViewModel();
model.setTitle("MEPER SPC Chart");

List<SpcChartData> datas = new ArrayList<>();
SpcChartData data = new SpcChartData();
final List<Double>[] xList = new List[]{Lists.newArrayList(0.65, 0.75, 0.75, 0.60, 0.70, 0.60, 0.75, 0.60, 0.65, 0.60, 0.80, 0.85, 0.70, 0.65, 0.90, 0.75, 0.75, 0.75, 0.65, 0.60, 0.50, 0.60, 0.80, 0.65, 0.65)};
final Double[][] stringArray = {new Double[xList[0].size()]};
for (int i = 0; i < xList[0].size(); i++) {
    stringArray[0][i] = xList[0].get(i);
}
data.setyData(stringArray[0]);
String[] yArr = new String[]{"2024-05-10", "2024-05-11", "2024-05-12", "2024-05-13", "2024-05-14", "2024-05-15",
        "2024-05-16", "2024-05-17", "2024-05-18", "2024-05-19", "2024-05-20", "2024-05-21",
        "2024-05-22", "2024-05-23", "2024-05-24", "2024-05-25", "2024-05-26", "2024-05-27",
        "2024-05-28", "2024-05-29", "2024-05-30", "2024-05-31", "2024-06-01", "2024-06-02",
        "2024-06-03"};
data.setxData(yArr);
data.setName("DataSPC");
data.setShowlegend(false);
data.setLineWidth(2);
data.setMarkerWidth(8);
data.setShowText(false);
List<SpcChartDataPoint> dataPoints = new ArrayList<>();
SpcChartDataPoint point = new SpcChartDataPoint();
point.setMarkerText("Limit limit Error!");
point.setxData("2024-05-23");
dataPoints.add(point);
data.setPoints(dataPoints);
datas.add(data);
model.setDatas(datas);
LumosSpcChart spc = new LumosSpcChart();
spc.initChart(model);
..\images\spc\image 2024 07 23 12 58 01 699
  • SpcChartBaseLine

SPC标注线数据模型

public class SpcChartBaseLine {

    //Y轴标准值
    private String value;

    //图例名称
    private String name;

    //标准线颜色
    private String color = SpcColor.RED.getHex();

    //标准线类型 直线/点线
    private String type = SpcChartLineType.dash.toString();

    //线条粗细
    private int width = 1;

    //是否显示图例
    private boolean showlegend = true;

}
  • 标准线渲染

List<SpcChartBaseLine> lines = new ArrayList<>();
SpcChartBaseLine LCLline = new SpcChartBaseLine();
LCLline.setName("RYAN");
LCLline.setType(SpcChartLineType.dash);
LCLline.setValue("0.6");
LCLline.setColor(SpcColor.ORANGE);
LCLline.setWidth(3);

SpcChartBaseLine UCLline = new SpcChartBaseLine();
UCLline.setName("EVEN");
UCLline.setType(SpcChartLineType.line);
UCLline.setValue("0.82");

SpcChartBaseLine center = new SpcChartBaseLine();
center.setName("CENTER");
center.setType(SpcChartLineType.dash);
center.setValue("0.71");
center.setColor(SpcColor.GRAY);


lines.add(LCLline);
lines.add(UCLline);
lines.add(center);
model.setLines(lines);
spc.initChart(model);
..\images\spc\image 2024 07 23 13 02 27 918
  • SpcErrorMessage

错误信息提示用于提示信息渲染

public class SpcErrorMessage {

    //提示信息颜色
    private String color = SpcColor.RED.getHex();

    //提示信息,若多条则换行显示
    private List<String> message;

    //字体大小
    private String size = SpcSize.SMALL.getSize();

}

单条或者多条提示信息展示

SpcErrorMessage errorMessage = new SpcErrorMessage();
List<String> errors = new ArrayList<>();
errors.add("opps~~ MAX limit 哦opps吼这个点错了,跑到下面来了");
errorMessage.setMessage(errors);
model.setErrorMessage(errorMessage);
spc.initChart(model);
..\images\spc\image 2024 07 23 13 06 34 422

10. Dashboard 看板更新内容

在最新的Release的MEPER 5.7.1版本中,针对看板产品的易用性我们做了针对性的改善。

10.1. Dashboard 主题切换功能

在智能看板中可以直接获取MEPER中配置的主题,并且可以切换主题直接更换所有的组件配色。

..\images\dashboard\image 2024 11 05 13 42 05 310

10.2. MEPER 配置 Dashboard主题

MEPER 新增配置主题功能,并且可以支持针对每个组件进行属性配置如下图所示,Dashboard可以直接使用主题,并修改所有组件的属性配置。

..\images\dashboard\image 2024 11 05 13 46 29 627

10.3. Dashboard 内置主题配色

我们将把常用的看板主题内置进入系统,以减少实施过程中对UI的定制化开发,内置主题搭配如下:

..\images\dashboard\image 2024 11 05 13 38 40 754
..\images\dashboard\image 2024 11 05 13 39 27 676

10.4. Dashboard 自适应显示优化

前版本的Dashboard是通过长宽定义最终显示效果,出现跨不同设备的屏幕分辨率展示时会出现显示异常的情况。 现版本重新设计自适应方案,直接设置屏幕宽高比例不直接设置宽高像素,在Dashboard渲染时直接根据当前屏幕宽度进行自适应计算并展示。

..\images\dashboard\image 2024 11 05 14 01 37 421

10.5. uniApp打包apk

apk打包的外壳后可以直接通过配置看板名称进行对应路由的跳转。

..\images\dashboard\image 2024 11 05 14 06 05 282

11. Api Space

ApiSpace接口功能组件,通过采用自定义XML格式文件,实现了远程接口与第三方数据库请求的标准化定义。这一创新减少了跨系统数据对接所需的代码冗余。通过简洁的XML文件,用户可以轻松记录接口定义,并直接进行接口调用。此外,通过与标准化的REP接口对接,将常见的接口操作以XML形式标准化记录,极大地提升了系统开发中的接口复用性。在需要进行简单的逻辑调整时,用户可以直接对XML文件进行版本更新或修改,从而显著降低了定制化接口开发过程中的代码研发成本。

11.1. Restful

ApiSpace平台目前提供了对自定义Restful接口请求的全面支持,包括灵活配置POST和GET请求的参数。这一功能不仅符合当前主流的服务接口设计标准,而且能够满足市场上大多数系统设计的需求。

此外,ApiSpace还允许用户自定义GET请求的目标路由,以及在GET请求过程中所需的Parameter参数。它还支持在请求过程中定义必要的Header和Cookie属性,以确保接口调用的灵活性和安全性。以下图示将为您展示这些功能的实现方式。

..\images\apispace\image 2024 11 05 14 11 43 627

POST请求接口,ApiSpace则支持在XML文件中直接配置JSON字符串,并且在字符串中可以自定义参数配置,在实际调用过程中,MEPER平台将会根据XML的配置替换JSON字符串中对应的请求参数。目前MEPER的ApiSpace还支持多种POST请求的ParameterMedia。如下图:

..\images\apispace\image 2024 11 05 14 14 37 080

MEPER平台在设计时充分考虑到了与第三方系统对接时可能面临的接口开发成本问题。为了降低这一成本,我们提供了一个便捷的解决方案:用户可以直接在MEPER平台中配置第三方系统的数据库连接。借助ApiSpace,用户可以轻松地通过MEPER平台接入第三方数据源,执行数据库表的查询操作,并将查询结果直接写入MEPER平台的指定数据库表中。此外,ApiSpace还支持直接进行业务逻辑处理,从而在第三方系统不提供接口开放的情况下,有效减少跨系统开发的成本和复杂性。这一创新功能不仅提高了系统的灵活性,也大大提升了开发效率。配置功能如下图所示:

..\images\apispace\image 2024 11 05 14 14 55 538

ApiSpace,作为MEPER框架的核心组件之一,无需额外开发和部署定制化代码,便能实现跨系统、跨平台以及跨数据库的数据获取。它通过Resultful和DBLink两种强大的数据获取能力,能够高效地从不同来源提取数据。获取数据后,ApiSpace能够依据XML配置文件,对数据进行定制化处理。通过这些配置文件,ApiSpace能够调用MEPER平台提供的开放方法,对每个数据字段进行精确的校验和转换,并将处理结果通过特定的方法进行进一步处理。

此外,ApiSpace内置了多种数据校验标准,并支持用户配置定制化的错误返回信息和错误处理机制,从而确保数据处理的准确性和可靠性。

通过将复杂的逻辑配置化和标准化,ApiSpace将处理逻辑以XML文件的形式保存,使得这些配置文件可以在任何MEPER相关系统中重复使用,进行逻辑处理。这种设计实现了低代码甚至无代码化的跨平台和跨系统能力,极大地简化了开发流程,提高了开发效率和系统的灵活性。

12. 消息推送和提醒

MEPER 571消息推送优化后实现

MEPER在571版本优化了PC和Mobile消息推送和菜单提醒功能。

Properties配置

meper.user.menu.notice.frequency=20000 #消息轮训间隔,目前支持最低更新间隔 2000ms

meper.user.menu.notice.enable=true #消息推送开关

meper.user.menu.notice.click.confirm=false #PC端菜单消息红点消失内置逻辑,点击后消除,默认false

12.1. PC端效果

..\images\notice\image 2024 11 05 15 57 58 540

PC端支持给每个菜单增加消息提醒,如上图中的红点,每个菜单的红点值就是通知数量,父级菜单统一使用 ! 来标识。

且PC端也支持消息推送,消息区分类型来展示不同配色样式,如上图所示五种类型。

红点的点击消除逻辑配置默认false,若想要点击后自动消除,则修改配置参数

meper.user.menu.notice.click.confirm=true
..\images\notice\image 2024 11 05 15 59 01 791

推送的消息若是不手动点击关闭的情况下,默认会在轮训的时间间隔一直推送

meper.user.menu.notice.frequency=20000 #消息轮训间隔,目前支持最低更新间隔 2000ms

当用户手动点击消息关闭的话, 则会停止推送对应点击的那条消息,逻辑是删除了当前点击的那条消息推送

12.2. Mobile端效果

..\images\notice\image 2024 11 05 15 59 29 985

移动端菜单优化,支持菜单组折叠。折叠后的红点不会在菜单组提醒,但是底部父级菜单依然会显示总数量。 通知移动端也支持五种对应的消息提醒类型。配色方案和PC端一致,且支持多消息堆叠。

移动端红点的数量刷新目前仅支持动态刷新页脚主菜单数量,不支持实时刷新子菜单(切换主菜单时会自动刷新数量)涉及到实时刷新动态页面逻辑,暂无实现方案。

移动端红点不支持点击消除,由于移动端的特殊性,没有办法确认用户点击菜单是否是需要消除红点还是仅仅菜单切换

所以需要业务自定调用Service消除移动端红点

消息确认

推送的消息若是不手动点击关闭的情况下,默认会在轮训的时间间隔一直推送

meper.user.menu.notice.frequency=20000 #消息轮训间隔,目前支持最低更新间隔 2000ms

当用户手动点击消息关闭的话, 则会停止推送对应点击的那条消息,逻辑是删除了当前点击的那条消息推送

移动端没有对应的关闭按钮,用户直接点击消息本身就是确认行为, 点击消息后,默认删除当前消息不会继续推送。

13. FAQ(二次开发中常见问题答疑)

13.1. 事件监听

(一)EventBus.register(EventListener)添加消息监听后,重复收到多条消息。

调用EventBus.register()监听事件,同一个事件不要每次都new一个新的EventListener对象来处理, 否则一次消息发布会出现多个消息监听对象。 适当时可用调用EventBus.unregister移除监听对象。

13.2. 数据库

(一)如何关闭Liquibase。
平台Liquibase功能是默认开启,如果关闭, 可在配置文件里面配置
lumos.liquibase.enable=false
(二)Oracle执行Liquebase报错。
当首次在Oracle上执行Liqueb的脚本时, 可能会遇到执行错误::SYS_USER_MODIFY_COLLATE::system failed,这是由于当前用户没有权限,需要执行以下脚本
 grant select on v_$parameter to mesdaqsi

14. NOTE(开发注意事项)

14.1. 影响性能的常见问题及其解决方案

14.1.1. DB相关

(一)为常用的查询字段添加索引。
(二)能用一张表的尽量使用一张表,必要时加一些冗余字段。

14.1.2. 典型问题

(一)查询数量时,不要查询对象集合的大小。

问题代码示例如下:

public int getUserCount() {
    return userDao.list(0, Integer.MAX_VALUE).size();
}

参考建议:使用count直接查询数量

public int getUserCount() {
    SimpleSqlQuery query = new SimpleSqlQuery("SYS_USER");
    return baseDao.countBySqlQuery(query);
}
(二)尽量避免一次性加载所有数据。

尽量避免“从后台取出所有数据,然后在java中对数据进行排序、过滤等操作”,建议在数据库层面,使用SQL语句等规则将数据一次性查询出来。

例如: 联表查询时,先查A表所有对象集合,然后遍历A的集合取出其中的字段,最后再以A为条件去查询B表数据。
问题代码示例:

IDomainObjectGrid<PackageDefinition> grid = new PaginationDomainObjectList<>();

grid.setFilter(presenter.searchFilter( null, null));

public EntityFilter searchFilter(String partName, String partVer) {
    EntityFilter filter = packageService.createFilter();
    List<Part> partList = partService.listByFuzzyPartNumberAndRev(partName, partVer);(1)
    List<Long> partIds = partList.stream().map(Part::getId).collect(Collectors.toList());(2)
    if (StringUtils.isNotEmpty(partName) || StringUtils.isNotEmpty(partVer)) {
            filter.fieldIn(PackageDefinitionEntity.PART_ID, partIds);(3)
    }
    return filter;
 }
1 查询出所有的Part。
2 遍历所有的Part,取出ID列表。
3 使用Part的ID作为查询条件,查询PackageDefinition。

参考建议:

PackagePagingQuery filterQuery = new PackagePagingQuery();

IObjectListGrid<PackageDefinition> grid = new PaginationObjectListGrid<>(filterQuery); (1)

public class PackagePagingQuery implements IPagingQuery<PackageDefinition> {
    @Override
    public PageModel<PackageDefinition> list(PageInfo pageInfo) {
        return BeanManager.getService(IPackageDefinitionService.class).pagingFuzzyByPartNumberAndRev( partNumber, partRevision, pageInfo);(2)
    }
}
1 使用分页Grid。
2 使用分页查询。
(三)拿对象时,如果只需要拿一些简单的值,可使用系统中的 SimpleData 对象。
public class UserSimpleData extends SimpleData {

    private String firstName;
    private String lastName;

    public UserSimpleData() {
        super();
    }

    public UserSimpleData(long id, String name, String firstName, String lastName, String description) {
        super(id, name, description);
        this.firstName = firstName;
        this.lastName = lastName;
    }
}
(四)谨慎使用for循环等遍历操作

能使用saveAll()的,尽量避免使用for循环去save(),同样适用于delete、query等方法。

(五)可以写在Handler中的逻辑尽量不要写在Service中。
(六)方法中添加缓存。

14.1.3. 前端相关

(一) Grid 使用分页 。

当页面组件需要从DB获取数据时,禁止一次性加载所有数据。

常用的一些组件,如 GridTreeGridComboBoxTwinColGrid 等,建议使用分页。

平台提供的Grid分页组件: PaginationObjectListGrid (适用于任何对象)和 PaginationDomainObjectList(适用于平台Domain对象)+

问题代码示例:

Grid<Part> grid = new Grid<Part>();
grid.setDataProvider(DataProvider.ofCollection(partService.getAllParts()));

参考建议:

IDomainObjectGrid<Part> grid = new PaginationDomainObjectList<>();
grid.setServiceClass(IPartService.class);
(二) ComboBox 使用分页

问题代码示例:

ComboBox<User> cbManager = new ComboBox<User>();
cbManager.setItems(presenter.getAllUser ()); //一次性拿到了所有的User数据

参考建议:

  • 参考1:重写 DataProvider

ComboBox<User> cbManager = new ComboBox<User>();
cbContent.setDataProvider(DataProvider.fromFilteringCallbacks(query -> {
    EntityFilter createFilter = services.createFilter();
    createFilter.setMaxResult(query.getLimit());
    createFilter.setStartPosition(query.getOffset());//传入分页信息
    return (Stream<ObjectBaseImpl<?>>)services.listByFilter(createFilter).stream();
}, (query) -> {
    EntityFilter createFilter = services.createFilter();
    return services.countByFilter(createFilter);
}));
  • 参考2:使用 SearchFieldComboBox(此控件默认实现了分页)

SearchFieldComboBox<User> cbManager = new SearchFieldComboBox<User>();
cbManager.setObjectType(CoreserviceObjectType.User);
  • 参考3:使用 SearchField,并在选择框里使用分页Grid。

SearchField<User> sfManager = new SearchField<>();
sfManager.setSearchDialogClass(IUserSearchDialog.class);
//IUserSearchDialog中要使用分页Grid
(三) Tree/TreeGrid 使用分页

在构建 Tree/TreeGrid`时,需要重写 `TreeDataProvider ,并注意以下事项:

1) 必须重写 getChildCount 方法, 虽然不重写也可以工作,但是效率会非常低,因为在父类中,他会调用fetchChilden().count(), 这也就意味着fetchChildren会被调用两边,两次从数据库加载数据,效率很低。但是如果你自信TreeData已经做了缓存,也没有问题,前提你要确保是这样。
2) 对于不需要前端缓存,每次都是从后台获取数据的provider,没有必要从TreeDataProvider继承,直接从AbstractHierarchicalDataProvider继承即可。
3) getChildCount()在重写的时候,严禁使用getChildren().size() 来获取数量,而是通过countChildren()等方法。
4) hasChildren()方法在实现的时候,严禁只用getChildren()再判空,需要直接后台有对应的获取数量的方法。

(四) Grid 渲染列时,避免 有大量的后台逻辑

当有很多后台逻辑时,尽量重写页面对象,将其封装成VO,从后台将关联数据一次性获取。

问题代码示例如下:

gridOrder.addColumn(source-> {
    List<WorkOrderItem> items = source.getOrderItems();
    long orderQty = 0;
    if (!items.isEmpty()) {
        orderQty = items.get(0).getQuantity();
    }
    .......有大量逻辑
    return ....;
});
(五) 页面的init()方法,尽量不要有大量的数据初始化操作。

加载数据的逻辑建议写在enter()方法中。

(六)慎用 @inject /@Autowired 注解

前端页面引入一些“需要加载很多数据”的组件时,尽量避免使用@inject注解,建议当使用此组件时,使用BeanManager.getService()来获取。

private AddAreaDialog addAreaDialog;

public AddAreaDialog getAddAreaDialog() {
        if (addAreaDialog == null) {
            addAreaDialog = BeanManager.getService(AddAreaDialog.class);
        }
        return addAreaDialog;
    }

//使用
getAddAreaDialog().show(this.getUI(), null);
(七)EntityFilter与DynamicEntityFilter 使用不规范

这两个Filter在使用的时候,要注意过滤条件的调用顺序。这个就跟你在写SQL语句的时候是一样的,条件A在前和条件B在前的效果是不一样的,

例如:

filter.fieldEqualTo(CheckItemGroup.ITEM_TYPE, obj.getItemType());
filter.fieldEqualTo(CheckItemGroup.NAME, obj.getName());
filter.fieldEqualTo(CheckItemGroup.SITE_ID, obj.getSiteId());

filter.fieldEqualTo(CheckItemGroup.SITE_ID, obj.getSiteId());
filter.fieldEqualTo(CheckItemGroup.ITEM_TYPE, obj.getItemType());
filter.fieldEqualTo(CheckItemGroup.NAME, obj.getName());

以上所产生的效果有可能是完全不一样的,取决于后续你们索引如何建立。但是一开始,书写方式要定下来,避免到最后索引无法建立或者必须建立更多索引,影响性能,请各个研发主管重视这个情况。

(八)尽量使用索引进行唯一性约束检查,而不是保存之前先到数据库里面做检查是否存在

14.1.4. JProfiler

当发现性能问题时,可使用JProfiler做性能分析,方便快捷。