Skip to content

lwliang/avalon

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

avalon-erp

介绍

odoo java版 前后分离快速开发平台,基于开源技术栈精心打造,融合Vue3+SpringBoot。 适配数据库mysql,postgres 支持标准的RAID权限功能,

社区

wechat_group

视频教程

环境搭建与模块开发:https://www.bilibili.com/video/BV1xBdhYCEQW/?spm_id_from=333.1387.homepage.video_card.click&vd_source=6b5f63a07c8f986c5f54a14dcf2cbe1b

ORM介绍:https://www.bilibili.com/video/BV1xBdhYCEeK/?spm_id_from=333.1387.homepage.video_card.click&vd_source=6b5f63a07c8f986c5f54a14dcf2cbe1b

前端介绍:https://www.bilibili.com/video/BV1xBdhYCEKY/?spm_id_from=333.1387.homepage.video_card.click

绿色版

Avalon 绿色版 0.1.1 内含数据库,nacos,redis,avalon运行环境,可以一键运行,支持window

百度云:https://pan.baidu.com/s/1QpnS9NAwbfDkClM9p8rRFw?pwd=zzxf 提取码: zzxf

天翼:https://cloud.189.cn/web/share?code=7jAzIviqeEFv(访问码:6dfi)

开发环境准备

1、安装docker

下载网址:https://www.docker.com/

2、安装redis

# 1. 拉取 Redis 7.0.4 镜像
docker pull redis:7.0.4
# 2. 启动 Redis 容器
docker run -d --name redis-7.0.4 -p 6379:6379 redis:7.0.4

3、安装Nacos配置中心

# 1. 拉取 Nacos 2.3.X 版本
docker pull nacos/nacos-server:v2.3.0
#  2. 启动 Nacos 容器
docker run -d --name nacos -p 8848:8848 -e MODE=standalone nacos/nacos-server:v2.3.0

4、安装postgres数据库

# 1. 拉取 PostgreSQL 镜像 版本不限制
docker pull postgres
# 2. 启动 PostgreSQL 容器
docker run -d --name postgres-container -p 5432:5432 \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=postgres \
postgres

5、连接到postgres容器并创建avalon账户

docker exec -it postgres-container psql -U postgres
CREATE ROLE avalon WITH LOGIN CREATEDB PASSWORD 'avalon';
# 验证角色创建
\du

6、本地环境安装jdk15+

教程:https://docs.pingcode.com/baike/2874339

7、使用IDEA打开avalon-java项目

7.1、在avalon-core目录下配置数据库

username改为avalon,password改为avalon

image-20250326145531489

7.2、在avalon-core目录下配置redis

一般情况下不用修改

image-20250326150208136

7.3、在avalon-erp目录下配置nacos

找到对应的application.yml文件,修改nacos对应的username与password,默认情况下不用修改,如果有修改avalon-file与avalon-im项目相同的文件,进行一样的调整

image-20250326150536574

8、启动avalon-erp项目

启动时需要增加如下参数,否则会报java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int) throws java.lang.ClassFormatError accessible: 错误

--add-opens java.base/java.lang=ALL-UNNAMED

image-20250326150857074

恭喜你成功运行avalon-erp项目,之后一次运行其他项目

运行Avalon-admin项目

1、安装node 20+版本

安装方法:https://blog.csdn.net/qq_37523550/article/details/136183405

2、使用WebStorm打开avalon-admin

执行yarn install 安装依赖

3、 使用yarn dev 运行软件

登录界面如下

image-20250326151810515

4 、使用webStrom进行调试

image-20250407115830116

架构

前端分离

后端采用微服务架构,

模块高内聚,低耦合方式,可继承的开发方式,大大提高开发效率,

支持mysql/postgres多数据库开发连接

目录结构图

image-20250408105601698

模块目录

以base模块为例

base(模块名)
│   BaseModule.java(模块类)
│
└───controller(http接口)
│   │   BaseController.java
│   
└───resource(资源文件,含菜单,视图,图片,默认数据)
|   │   record (默认数据)
|	│   |	base.group.xml(base.group模型默认数据)
|	|	view (视图与菜单)
|	|	|	menu.xml (菜单)
|	|	|	base.service.views.xml (base.service模型视图)
|    
|	service(模型)
|	|	userService.java (用户模型)
|	|

案例模块

在这里可以学会如何在avalon上创建一个属于自己的模块,此模块功能有房租出租功能

创建house模块

在avalon-erp/src/main/java/com/avalon/erp/addon包下创建house包

image-20250408112006350

创建HouseModule模块类

在house包下创建HouseModule.java类,并且继承AbstractModule类

package com.avalon.erp.addon.house;

import com.avalon.core.module.AbstractModule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * @author [email protected]
 * @date 2025/04/08 11:24
 */
@Component
@Slf4j
public class HouseModule extends AbstractModule {
    @Override
    public String getModuleName() { // 模块标识 唯一值
        return "house";
    }

    @Override
    public String getLabel() { // 显示标题
        return "租房";
    }

    @Override
    public String getDescription() { // 描述
        return "租房,看房等功能";
    }

    @Override
    public Boolean getDisplay() { // 安装后,显示在左边栏位上
        return true;
    }
}

image-20250408112712305

创建HouseService模型

在house包下,创建service包,以及在service包下创建HouseService类,并且继承AbstractService类.

模型自带id,name,createTime,creator,updateTime,updater字段

package com.avalon.erp.addon.house.service;

import com.avalon.core.service.AbstractService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * @author [email protected]
 * @date 2025/04/08 11:29
 */
@Service
@Slf4j
public class HouseService extends AbstractService {
    @Override
    public String getServiceName() { // 模型名,等价于表名,第一个house表示模块名
        return "house.house";
    }

    @Override
    public String getLabel() {
        return "房屋";// 标题
    }
}

image-20250408113116589

创建视图资源包

在house包下,创建resource/view包

image-20250408113450295

创建HouseService视图文件

在resource/view下创建house.house.views.xml视图文件

在视图文件中,需要生成窗口,tree视图,form视图

<?xml version="1.0" encoding="UTF-8" ?>
<avalon>
    <!--house form 视图-->
    <record id="house_house_view_form" service="base.action.view">
        <field name="name">house form</field>
        <field name="label">房屋</field>
        <field name="viewMode">form</field>
        <field name="ref_serviceId">house.house</field>
        <field name="arch" type="xml">
            <form>
                <sheet>
                    <row>
                        <col>
                            <field name="name"/>
                        </col>
                    </row>
                </sheet>
            </form>
        </field>
    </record>

    <!--house tree 列表视图-->
    <record id="house_house_tree" service="base.action.view">
        <field name="name">house list</field>
        <field name="label">房屋</field>
        <field name="viewMode">tree</field>
        <field name="ref_serviceId">house.house</field>
        <field name="arch" type="xml">
            <tree>
                <field name="name"/>
            </tree>
        </field>
    </record>

    <!--house窗口-->
    <record id="house_house_action" service="base.action.window">
        <field name="name">house</field>
        <field name="label">房屋信息</field>
        <field name="viewMode">tree</field>
        <field name="ref_serviceId">house.house</field>
    </record>
</avalon>

创建菜单

在resource/view下创建menu.xml菜单文件

<?xml version="1.0" encoding="UTF-8" ?>
<avalon>
    <menuitem id="menu_house_house_action" name="房屋" action="house_house_action"/>
</avalon>

将视图,菜单文件与模块类进行绑定

资源文件需要与模块类进行绑定,否则无法生效

package com.avalon.erp.addon.house;

import com.avalon.core.module.AbstractModule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * @author [email protected]
 * @date 2025/04/08 11:24
 */
@Component
@Slf4j
public class HouseModule extends AbstractModule {
    @Override
    public String getModuleName() { // 模块标识 唯一值
        return "house";
    }

    @Override
    public String getLabel() { // 显示标题
        return "租房";
    }

    @Override
    public String getDescription() { // 描述
        return "租房,看房等功能";
    }

    @Override
    public Boolean getDisplay() { // 安装后,显示在左边栏位上
        return true;
    }

    // 菜单文件在最后
    @Override
    public String[] getResource() {
        return new String[]{
                "resource/view/house.house.views.xml",
                "resource/view/menu.xml",
        };
    }
}

启动avalon-erp,avalon-file,avalon-admin项目

使用IDEA,webstorm启动项目

使用前端创建demo数据库

项目启动后,在登录页面,点击 管理数据库 按钮

image-20250408115700557

点击创建数据库按钮,输入demo,点击确认,等待一段时间,数据库会自动创建完毕

image-20250408115807706

登录系统

数据库创建之后,点击数据库名字,会跳转到登录页面,

默认管理员账户admin,密码是123456

image-20250408115936400

刷新模块

如果模块没有显示,则点击App菜单下的更新模块菜单,进行刷新模块,刷新后按F5刷新当前页面

image-20250408120036893

image-20250408120133009

安装租房模块

点击安装即可

image-20250408120200173

image-20250408120225317

恭喜已经完成house模块的开发

支持的字段

  1. 基本字段
BigDecimal,
BigInteger,
Boolean,
Date,
DateTime,
Double,
Float,
Html,
Image,
Integer,
Selection(Enum),
String,
Text,
Time,
Password
  1. 关联字段
One2one, 1对1
One2many, 1对多
Many2one,多对1
Many2many 多对多

Field字段属性

公用属性

public interface IField {
    //返回数据类型对应的数据库类型字段
    Integer getSqlType();

    // 获取字段所在模型
    AbstractService getService();

    // 唯一值
    Boolean isUnique();//是否是唯一的值

    /**
     * 唯一
     *
     * @param isUnique 唯一
     */
    void setIsUnique(Boolean isUnique);

    // 允许为null
    Boolean allowNull();//是否允许为空

    /**
     * 设置可以为空值
     *
     * @param allowNull 空值
     */
    void setAllowNull(Boolean allowNull);

    // 字段名称,即属性名,也是数据库字段名 使用驼峰
    String getName();

    //数据库名称
    String getFieldName();

    // 主键
    Boolean isPrimaryKey();

    // 自增
    Boolean isAutoIncrement();

    //默认值
    IFieldDefaultValue getDefaultValue();

    void setDefaultValue(IFieldDefaultValue defaultValue);

    // 必填
    Boolean isRequired();

    void setIsRequired(Boolean isRequired);

    // 只读
    Boolean isReadonly();

    Type getFieldType();

    Object getSqlValue(Object value);

    Object getClientValue(Object value); // 得到前端显示的值

    String getClassType();
}

Integer字段

  public Integer getMax() // 最大值
  public Integer getMin() //最小值

String字段

 public Integer getMaxLength() // 最大长度
 public Integer getMinLength() // 最小长度

ORM API

Module类

	//模块名
    public abstract String getModuleName();

	//模块名称
    public abstract String getLabel();

	//描述
    public abstract String getDescription();

    /// 安装之后,是否显示在菜单中
    public abstract Boolean getDisplay();

	// 依赖模块,安装本模块之前,先安装依赖模块
    public String[] depends() 

    /**
     * 自动安装,true,则depends的模块已安装,则自动安装当前模块,未完成
     *
     * @return
     */
    public Boolean autoInstall()


    /**
     * 模块安装之后,运行js
     *
     * @return js路径
     */
    public String[] getStartJS()

	// 前端web依赖的vue组件,实验阶段
    public String[] getVue() 

    /**
     * 模块图标
     *
     * @return url 本地文件
     */
    public String getIcon()
        
    /**
     * 创建模块
     */
    public void createModule() 
   
     // 获取模块的所有模型类
    public AbstractServiceList getServiceList() 
    
    // 根据模块名获取模块主键
    public Integer getModuleId(String moduleName) 
        
    // 删除模块
    public void dropModule() 
    
    // 升级模块
    public void upgradeModule()

AbstractService模型类

查询接口

    /**
     * 查询主键集合
     *
     * @param condition
     * @return
     * @throws AvalonException
     */
    Record search(Condition condition) throws AvalonException;

    /**
     * 统计个数
     *
     * @param condition
     * @return
     * @throws AvalonException
     */
    Integer selectCount(Condition condition) throws AvalonException;

	// 查询满足条件的记录
    Record select(Condition condition, String... fields) throws AvalonException;
	// 查询满足条件的记录
    Record select(String order, Condition condition, String... fields) throws AvalonException;
	// 查询满足条件的记录
    Record select(Integer limit, String order, Condition condition, String... fields) throws AvalonException;
	// 查询满足条件的记录 分页
    PageInfo selectPage(PageParam pageParam,
                        String order,
                        Condition condition,
                        String... fields) throws AvalonException;

    //获取字段值 从数据库获取
    FieldValue getFieldValue(String fieldName, Condition condition);

创建接口

// 创建默认记录
RecordRow create(RecordRow defaultRow) throws AvalonException;

新增接口

PrimaryKey insert(RecordRow recordRow) throws AvalonException;//插入记录 会检查当前记录 及联插入
List<Object> insertMulti(Record record) throws AvalonException;//批量插入记录 会检查当前记录 及联插入

更新接口

Integer update(RecordRow recordRow, Condition condition) throws AvalonException;//更新记录 直接更新

Integer update(RecordRow recordRow) throws AvalonException;//更新记录 检查满足更新条件 及联更新

Integer updateMulti(Record record) throws AvalonException;//批量更新

删除接口

Integer delete(Object id) throws AvalonException;//删除指定主键记录 不会检查是否满足删除条件, 直接删除

Integer delete(Condition condition, String serviceName) throws AvalonException;//条件删除 会检查记录是否存在

Integer delete(RecordRow row) throws AvalonException;//删除记录 会检查当前记录 及联删除

调用服务接口

// 调用服务方法
Object invokeMethod(String service, String methodName, Object... args) 

OnChange注解

前端数据修改之后,会触发模型方法调用

例子:

    @OnChange("active") // 单字段修改
    public ChangeRecordRow onChangeActive(RecordRow newRow, RecordRow oldRow) {
        log.info("active is modified");
        return new ChangeRecordRow();
    }

    @OnChange("serviceAccess") // 如果是one2many,则会整个字段触发,含新增,删除,某行的字段修改
    public ChangeRecordRow onChangeServiceAccess(RecordRow newRow, RecordRow oldRow) {
        log.info("serviceAccess is modified count=" + newRow.getRecord(serviceAccess).size());
        return new ChangeRecordRow();
    }
   @OnChange({"debug", "name"}) // 支持多个字段 返回值有value是个键值对,则会覆盖前端的值,warings则会前端报错提醒
    public ChangeRecordRow onDebugNameChange(RecordRow newRow, RecordRow oldRow) {
        log.info("3" + newRow.getString("name").toString());
        ChangeRecordRow changeRecordRow = new ChangeRecordRow();
        if (!newRow.getString("name").contains("_java")) {
            newRow.put("name", newRow.getString("name") + "_java");
            changeRecordRow.addWarning("错误", "名字必须含_java");
        }
        changeRecordRow.setValue(newRow);

        return changeRecordRow;
    }

结果:

image-20250507173431511

数据库接口

 	/**
     * 创建数据库表
     */
    public void createTable() 
    /*
     删除数据库表
     */
    public void dropTable()
    /*
     数据库表是否存在
     */
    public Boolean existTable() 
    /*
    数据库字段是否存在
     */
    public Boolean existField(Field field)
    /*
    删除数据库字段
     */
    public void dropField(String fieldName)
    
    /**
     * 升级数据表结构
     */
    public void upgradeTable()

TransientService模型

用于记录临时记录,不会生成数据库结构

主要用于,用于定制Form视图

//进行文档显示
@Service
@Slf4j
public class DocumentViewService extends TransientService {
    @Override
    public String getServiceName() {
        return "document.show.transient";
    }

    @Override
    public String getLabel() {
        return "文档显示";
    }

    public Field documents = Fields.createOne2many("文档", "document.file", "ownerId"); //当前用户文档

    @Override
    public RecordRow create(RecordRow defaultRow) throws AvalonException {
        AbstractService documentService = getContext().getServiceBean("document.file");
        Condition condition = Condition.equalCondition("ownerId", getContext().getUserId())
                .andEqualCondition("active", false)
                .andEqualCondition("parentId", null); // 默认获取第一层文件与文件夹
        Record select = documentService.select(condition,
                "id", "name", "isFolder", "url", "size", "mine", "ownerId");
        defaultRow.put(documents, select);
        return super.create(defaultRow);
    }

    // 上传文件,前端跳转路由
    public RecordRow uploadFile() {
        RecordRow row = RecordRow.build();
        row.put("type", "ir.actions.client")
                .put("tag", "uploadDocument");
        return row;
    }
}

继承

继承分为委托继承、扩展继承,原型继承

委托继承

委托继承:是一种通过为模型添加外键(Many2one 字段)指向另一个模型来实现的继承方式。

  1. 模型独立性:委托继承的子模型不会直接继承父模型的字段和方法,而是通过外键(Many2one)字段关联到父模型。
  2. 不修改父模型:父模型保持独立,子模型通过外键引用父模型来获得其字段和方法。
  3. 数据分离:父模型和子模型的数据分别存储在各自的数据库表中,通过外键关联。
  4. 适用场景:当你需要逻辑上关联两个模型,但不希望直接修改或扩展父模型时,可以使用委托继承。

相关接口

    /**
     * 委托继承
     * 格式 serviceName: field
     *
     * @return 继承模型
     */
    DelegateInheritMap getDelegateInherit();

    /**
     * 获取委托继承字段
     *
     * @return 字段列表
     */
    FieldList getDelegateInheritFields();
	/**
     * 获取委托模型下的所有字段
     * @param delegateServiceName 委托继承模型
     * @return 字段
     */
    FieldList getDelegateInheritFields(String delegateServiceName);

    /**
     * 判断是否是委托字段
     *
     * @param fieldName 字段名
     * @return 是 否
     */
    boolean isDelegateInheritField(String fieldName);

用例

@Slf4j
@Service
public class StaffService extends AbstractService {
    @Override
    public String getServiceName() {
        return "hr.staff";
    }
	
     /**
     * 委托继承
     * 格式 serviceName: field
     *
     * @return 继承模型
     */
    @Override
    public DelegateInheritMap getDelegateInherit() {
        DelegateInheritMap delegateInheritMap = new DelegateInheritMap();
        delegateInheritMap.put("crm.partner", "partnerId");
        return delegateInheritMap;
    }

    @Override
    public String getLabel() {
        return "员工";
    }

    public final Field partnerId = Fields.createMany2one("联系人", "crm.partner"); // 此字段为委托继承,可以通过当前模型,进行同步修改
    public final Field code = Fields.createString("员工编码");
    public final Field jobId = Fields.createMany2one("岗位", "hr.job");
    public final Field orgId = Fields.createMany2one("组织", "hr.org");
    public final Field userId = Fields.createMany2one("账号", "base.user");
}

扩展继承

扩展继承 允许你向现有模型添加字段、方法或重写现有方法,而无需直接修改原始模型代码。

扩展继承的特点 不修改原始模型:通过 getInherit接口 扩展现有模型,而不是修改其定义。 添加新字段:可以向现有模型添加自定义字段。 重写方法:可以通过基类调用原始方法并添加自定义逻辑。 适用范围广:适用于 avalon 的所有模型

接口

	/**
     * 继承模式 getServiceName == getInherit 则是扩展,否则是继承
     *
     * @return 继承模型
     */
    String getInherit(); // 只能单继承 因为java是单继承,无法实现多继承

    /**
     * 继承字段
     *
     * @return 继承字段
     */
    List<Field> getInheritFields();

案例

@Service
@Slf4j
public class HrUserService extends AbstractService {
    @Override
    public String getServiceName() {
        return "base.user"; // 必须和getInherit 保持一致
    }

    @Override
    public Boolean getNeedDefaultField() {
        return false;
    }

    @Override
    public String getInherit() {
        return "base.user";
    }

    public Field staffId = Fields.createMany2one("员工", "hr.staff");
}

原型继承

原型继承 是通过 getInherit 接口实现的。这种继承方式与扩展继承不同,它允许一个模型直接继承另一个模型的字段和方法,而不直接修改父模型。

原型继承的特点 字段共享:子模型可以直接访问父模型的字段,就像这些字段是子模型的一部分。 数据分离:父模型和子模型的数据存储在各自的表中 模型独立性:父模型和子模型是独立的模型,可以分别定义自己的字段和逻辑。 适用场景:当两个模型需要共享字段而又要保持数据库表独立时,使用原型继承。

接口

	/**
     * 继承模式 getServiceName == getInherit 则是扩展,否则是继承
     *
     * @return 继承模型
     */
    String getInherit(); // 只能单继承 因为java是单继承,无法实现多继承

    /**
     * 继承字段
     *
     * @return 继承字段
     */
    List<Field> getInheritFields();

案例

@Service
@Slf4j
public class HrUserService extends AbstractService {
    @Override
    public String getServiceName() {
        return "hr.user"; // 必须和getInherit不同
    }

    @Override
    public Boolean getNeedDefaultField() {
        return false;
    }

    @Override
    public String getInherit() {
        return "base.user";
    }

    public Field staffId = Fields.createMany2one("员工", "hr.staff");
}

视图

窗口

action.window 是一种操作类型,用于定义打开一个窗口(视图)的动作。它常用于为模型创建菜单或触发器,以便用户可以在界面中快速访问特定的数据记录或表单视图。

id 资源唯一值

service对应base.action.window

其中内部的field字段是对应模型中的字段,name属性则是字段名

其ref_开头表示是引用字段,比如ref_serviceId查找base.user对应的模型id

<record id="base_user_action" service="base.action.window">
        <field name="name">user</field>
        <field name="label">用户</field>
        <field name="viewMode">tree</field>
        <field name="ref_serviceId">base.user</field>
</record>

tree

ree 视图(也称为列表视图)是一种用于显示模型记录的表格形式的视图。

记录保存在base.action.view模型中

    <record id="base_user_view_tree" service="base.action.view">
        <field name="name">base_user_view_tree</field>
        <field name="label">用户</field>
        <field name="viewMode">tree</field>
        <field name="ref_serviceId">base.user</field>
        <field name="arch" type="xml">
            <tree>
                <field name="id"/>
                <field name="avatar"/>
                <field name="name"/>
                <field name="account"/>
                <field name="password"/>
            </tree>
        </field>
    </record>

例子

image-20250408213235236

增加顶部按钮

在base.user模型上增加demo按钮

    <record id="base_user_view_tree" service="base.action.view">
        <field name="name">base_user_view_tree</field>
        <field name="label">用户</field>
        <field name="viewMode">tree</field>
        <field name="ref_serviceId">base.user</field>
        <field name="arch" type="xml">
            <tree>
                <header>
                    <MyButton :rounded="true" type="primary" action="demoClick"
                              action-type="object">Demo
                    </MyButton>
                </header>
                <field name="id"/>
                <field name="avatar"/>
                <field name="name"/>
                <field name="account"/>
                <field name="password"/>
            </tree>
        </field>
    </record>

action-type="object":意思是调用模型的方法

action="demoClick":方法名

user模型代码

@Service
@Slf4j
@Primary
public class UserService extends AbstractService implements IUserService {
    @Override
    public String getServiceName() {
        return "base.user";
    }

   //....

    //参数param 会根据选中的记录的id列表,没有选中,则{},有则{ids:[1,2]}
	public RecordRow demoClick(RecordRow param) {
        return null; // 返回null 前端会提示操作成功
	}
}

前端未选择记录效果:

image-20250506180330036

前端选择记录效果:

image-20250506180442915

支持在tree视图中直接修改与新增记录

支持在tree视图的表格内修改,新增记录,而不用弹窗,这样的操作方式,适用于字段比较少的表

效果:

image-20250517115222147

用法:

<record id="base_user_view_tree" service="base.action.view">
        <field name="name">base_user_view_tree</field>
        <field name="label">用户</field>
        <field name="viewMode">tree</field>
        <field name="ref_serviceId">base.user</field>
        <field name="arch" type="xml">
            <tree editable="bottom"> <!--使用editable 属性 值bottom 新增的记录在下方,top在上方-->
                <field name="id"/>
                <field name="avatar"/>
                <field name="name"/>
                <field name="account"/>
                <field name="createTime"/>
                <field name="myTime"/>
                <field name="myDate"/>
            </tree>
        </field>
    </record>

form

Form 视图 是用于显示单条记录的详细信息的视图类型。它是 Avalon中最常用的视图之一,通常用于创建、编辑和查看记录的详细内容。通过 Form 视图,用户可以管理模型的所有字段,并定义交互式的用户界面。

记录保存在base.action.view模型中

 <record id="base_user_view_form" service="base.action.view">
        <field name="name">base_user_view_form</field>
        <field name="label">用户表单</field>
        <field name="viewMode">form</field>
        <field name="ref_serviceId">base.user</field>
        <field name="arch" type="xml">
            <form>
                <sheet>
                    <row>
                        <col>
                            <field name="name"/>
                            <field name="account"/>
                            <field name="password"/>
                        </col>
                        <col>
                            <field name="avatar"/>
                            <field name="debug"/>
                        </col>
                    </row>
                </sheet>
            </form>
        </field>
    </record>

例子

image-20250408213359301

kanban

Kanban 视图 是一种用于以卡片形式来显示模型记录的视图类型。Kanban 视图非常适合显示分组数据(没实现),并允许用户通过拖拽(没实现)的方式管理记录的状态(或其他属性)

记录保存在base.action.view模型中,同时template标签之外的字段才可以使用

 <record id="base_module_view_kanban" service="base.action.view">
        <field name="name">kanban</field>
        <field name="label">看板</field>
        <field name="viewMode">kanban</field>
        <field name="ref_serviceId">base.module</field>
        <field name="arch" type="xml">
            <kanban>
                <field name="name"/>
                <field name="label"/>
                <field name="description"/>
                <field name="isInstall"/>
                <field name="icon"/>
                <field name="createTime"/>
                <template>
                    <div class="pr-4 flex justify-center items-center mr-4">
                        <MyImage width="50" height="50" :src="getModuleIcon(name,icon)"/>
                    </div>
                    <div class="pr-4">
                        <div>
                            <div class="pb-0.5">{{ label }}</div>
                            <div class="text-gray-400">{{ description }}</div>
                        </div>
                        <div class="pt-4">
                            <MyButton :rounded="true" type="primary" :action="isInstall ? 'upgrade':'install'"
                                      action-type="object">{{ isInstall ? '升级' : '安装' }}
                            </MyButton>
                            <MyButton :rounded="true" class="ml-2" type="danger" v-if="isInstall" action="uninstall"
                                      action-type="object">卸载
                            </MyButton>
                        </div>
                    </div>
                </template>
            </kanban>
        </field>
    </record>

例子

image-20250408213149964

xtree视图

将模型分为左右两部分,左边是树状列表,右边是form视图,可点击左边记录,然后再form视图中进行修改。树状列表支持拖拽

配置

    <record id="hr_org_view_tree" service="base.action.view">
        <field name="name">hr_org_view_tree</field>
        <field name="label">组织</field>
        <field name="viewMode">xtree</field>
        <field name="ref_serviceId">hr.org</field>
        <field name="arch" type="xml">
            <xtree>
                <parentField name="parentId"/> <!--上级字段-->
                <nameField name="name"/> <!--显示字段-->
                <childrenField name="childIds"/> <!--下级字段列表-->
            </xtree>
        </field>
    </record>

image-20250504134820996

search视图

search视图一般在tree视图的顶部显示,用于搜索tree数据

例子:以下是在pet.train.item模型上增加search视图,可搜索name,petTypeIds.typeId,tag,creator,difficulty字段

注意:仅最多支持二级字段

   <record id="pet_train_item_view_search" service="base.action.view">
        <field name="name">pet_train_item_view_search</field>
        <field name="label">项目查询</field>
        <field name="viewMode">search</field>
        <field name="ref_serviceId">pet.train.item</field>
        <field name="arch" type="xml">
            <search>
                <field name="name"/> <!--String字段 使用like条件-->
                <field name="petTypeIds.typeId"/><!--many2many字段 使用like条件-->
                <field name="tag"/> <!--selection字段 使用like-->
                <field name="creator"/> <!--many2one字段 使用like-->
                <field name="difficulty"/> <!--integer字段 使用 = 条件-->
            </search>
        </field>
    </record>

效果如下:

image-20250506215446811

down视图

down作用于many2one的下拉界面中,当需要对某个模型的下来进行定制,则可以定义down视图,不定义,则会显示name字段列表

例子:

 <record id="pet_train_item_view_down" service="base.action.view">
        <field name="name">pet train item down</field>
        <field name="label">训练项目</field>
        <field name="viewMode">down</field>
        <field name="ref_serviceId">pet.train.item</field>
        <field name="arch" type="xml">
            <down>
                <field name="name"/>
                <field name="petTypeIds"/>
                <field name="tag"/>
                <field name="vip"/>
            </down>
        </field>
    </record>

页面效果:

a58cc0c29d3548579656b5e47d57eba

菜单

创建前端菜单入口,只能三级

创建模型窗口

<avalon>
    <menuitem id="menu_base_group_paren" name="权限">
        <menuitem id="menu_base_group_action" name="权限组" action="base_group_action"/>
        <menuitem id="menu_base_rule_action" name="记录规则" action="base_rule_action"/>
	</menuitem>
</avalon>

创建调用后台方法菜单

意图:点击之后 调用base.module模型下的refreshModuleFromDisk方法

<avalon>
	<menuitem id="menu_module_parent_action" name="App">
        <menuitem id="menu_module_refresh_action" serviceId="base.module" name="更新模块" type="object"
                  action="refreshModuleFromDisk"/>
    </menuitem>
</avalon>

在form视图的顶部创建按钮

按钮默认都是调用后台方法 调用document.show.transient模型的uploadFile方法,默认会带上当前id参数,如果是新增状态,则不传id参数

<record id="document_document_show_form" service="base.action.view">
        <field name="name">document show</field>
        <field name="label">文件</field>
        <field name="viewMode">form</field>
        <field name="ref_serviceId">document.show.transient</field>
        <field name="arch" type="xml">
            <form create="false" edit="false">
                <header>
                    <MyButton :rounded="true" type="primary" class="ml-2" action="uploadFile"
                              action-type="object">上传文件
                    </MyButton>
                </header>
                <sheet>
                    <field name="documents" widget="document"/>
                </sheet>
            </form>
        </field>
    </record>

调用前端方法

可以在调用后台方法时,返回指定格式的值,前端会根据情况进行前端调用

格式

{
    "type":"ir.actions.client", // 命令类型
    "tag":"uploadDocument", // 方法
    "param":{} // 参数,没有可以不传
}

例子

 // 上传文件,前端跳转路由
    public RecordRow uploadFile() {
        RecordRow row = RecordRow.build();
        row.put("type", "ir.actions.client")
                .put("tag", "uploadDocument");
        return row;
    }

创建模型记录

可以在数据库模型,创建时,在模型表中增加记录

创建模型记录

介绍创建模型的一般方式

在base模块的resource/record目录下新增对应模型的base.group.xml文件

image-20250515124753595

在base.group.xml文件添加如下内容

record标签的id表示资源id唯一值,service对应模型名称,field标签的name属性则是字段值,内容则对应的值。field都复制有很多方式

<?xml version="1.0" encoding="UTF-8" ?>
<avalon>
    <record id="base_group" service="base.group">
        <field name="name">基础权限组</field>
        <field name="active">true</field>
    </record>
    <!--介绍field复制方式-->
	<record>
    	 <field name="serviceId" eval="refServiceId('base.user')"/> <!--refServiceId 获取base.user模型的id-->
         <field name="groupId" eval="refId('base.base_group')"/><!--refId 获取 base模型下 base_group资源id-->
    </record>
</avalon>

base模块类上启用base.group.xml文件

image-20250515133237396

avlon-erp HTTP 接口

erp服务:http://localhost:8089/erp

登录

接口:/login

方式:POST

参数:

{
    "db":"avalon",
    "username": "admin",
    "password": "123456"
}

返回值:

{
    "id": 1, // 账户主键
    "db": "avalon",
    "token": "239b19788adc452ebe57e06f0ae95461" // 登录token
}

创建模型默认值

不会保存到数据库中,只是返回给前端

接口:url:/service/{serviceName}/create

方式:POST

参数:可传可不传,传了以参数为准返回默认值

{
    "value": {
        "name": "演示账号"
    }
}

返回值:serviceName=base.user

{
    "name": "演示账号"
}

插入模型记录

保存到数据库中

接口:/service/{serviceName}/add

方式:POST

参数 serviceName=base.user

{
	"value": {
        "name": "演示账号",
        "account": "demo",
        "password": "123456"
    }
}

返回值:

{
    "id":2//新增的主键
}

更新模型记录

接口:/service/{serviceName}/update

方式:POST

参数:serviceName=base.user

{
	value:{
        "id":1,// 主键 需要包括
        "{fieldName}":value,
        "one2ManyField":[
            {
                 "{fieldName}":value,
                "op":"insert|delete|update"
            }
        	]
    	}
	}
}

返回值: 原值返回

{
     "id":1,// 主键 需要包括
        "{fieldName}":value,
        "one2ManyField":[
            	{
                 	"{fieldName}":value,
            	    "op":"insert|delete|update"
            	}
        	]
    	}
}

删除模型记录

接口:/service/{serviceName}/delete

方式:POST

参数:serviceName=base.user

{
	id:1
}

返回值:

1// 删除的个数

查询模型详情

接口:/service/get/{serviceName}/detail

方式:POST

参数:serviceName=base.user

{
	"fields":"id,name,account",
    "condition":"('id',=,1)"
}

返回值:

{
    "name": "管理管理员",
    "id": 1,
    "account": "admin"
}

查询模型全部记录

接口:/service/get/{serviceName}/all

方式:POST

参数:serviceName=base.user

{
	"fields":"id,name,account",
    "condition":"('id',=,1)",
	"order":"id asc, name desc"
}

返回值:

[
    {
        "name": "管理管理员",
        "id": 1,
        "account": "admin"
    }
]

分页查询模型记录

接口:/service/get/{serviceName}/page

方式:POST

参数:serviceName=base.user

{
    "page": {
        "pageNum": 1,
        "pageSize": 10
    },
    "fields": "id,name,account",
    "condition": "('id',=,1)",
    "order": "id asc, account desc"
}

返回值

{
    "total": 1,
    "pageCur": 1,
    "pageSize": 10,
    "pageCount": 1,
    "nextPage": false,
    "prePage": false,
    "data": [
        {
            "name": "管理管理员",
            "id": 1,
            "account": "admin"
        }
    ]
}

获取模型的字段

接口:/service/get/{serviceName}/fields

方式:POST

参数:serviceName=base.user

{
   "field":"主键"// 模糊匹配lable,不传字段则获取全部
}
[
    {
        "isRequired": true,
        "isReadonly": true,
        "relativeServiceName": null,
        "defaultValue": "0",
        "maxValue": 2147483647.000000,
        "isUnique": false,
        "isPrimaryKey": true,
        "label": "主键",
        "type": "IntegerField",
        "manyServiceTable": null,
        "isAutoIncrement": true,
        "masterForeignKeyName": null,
        "minValue": -2147483648.000000,
        "name": "id",
        "allowNull": true,
        "id": 6,
        "serviceId": 1,
        "relativeForeignKeyName": null,
        "relativeFieldName": null
    }
]

获取模型的枚举字段取值范围

接口:/service/get/{serviceName}/selection/map

方式:POST

参数:serviceName=hr.org

{
 fields:"type" // 一个字段
}

返回值:

{
    "company": "公司",
    "department": "部门"
}

导出Excel文件

接口:/service/export/{serviceName}/excel

方式:POST

参数:serviceName=base.user

{
    "order": "",
    "field": "id,account,name",
    "condition": "('id',in,3,1)"
}

返回值:excel文件

读取excel的记录

接口:/service/read/{serviceName}/excel

接口:POST

参数:FormData

file:File

返回值:excel文件内容

{
    "headers": [
        "账号",
        "昵称"
    ],
    "data": [
        {
            "账号": "admin1",
            "昵称": "管理管理员1"
        },
        {
            "账号": "demo2",
            "昵称": "演示账号1"
        }
    ],
    "fields": [
        "account",
        "name"
    ]
}

导入模型记录

接口:/service/import/{serviceName}/excel

接口:POST

参数:

{
    "headers": [
        "账号",
        "昵称"
    ],
    "data": [
        {
            "账号": "admin1",
            "昵称": "管理管理员1"
        },
        {
            "账号": "demo2",
            "昵称": "演示账号1"
        }
    ],
    "fields": [
        "account",
        "name"
    ]
}

返回值:

{
    "imported": 2 // 成功导入的个数
}

condition

=|>|like|<|>=|<=|notLike|!=

"('field',=,2|'a'|1.1)"

between

"('field',between,1,2)"

in | notIn

"('field',in,1,2,3)"

取反

"!('field',=,2|'a'|1.1)"

交集

"('field',=,2)&('field',=,2)"

并集

"('field',=,2)|('field',=,2)"

avlon-erp 配置文件

application.yml 配置文件

server:
  port: 8090
  servlet:
    context-path: /erp
spring:
  banner:
    location: banner.txt
  profiles:
    active: dev,erp-dev
  application:
    name: avalon-server
  thymeleaf:
    cache: false
    prefix: classpath:/templates/
    encoding: UTF-8
    suffix: .html
    mode: HTML

application-erp-dev.yml配置文件

spring:
  profiles:
    host: localhost

pulsar:
  url: pulsar://${spring.profiles.host}:6650
  enable: false

avalon-core配置文件

application-dev.yml

application:
  datetime-format: yyyy-MM-dd HH:mm:ss # 系统日期时间格式,接口参数,返回值,数据库统一
  date-format: yyyy-MM-dd # 系统日期格式,接口参数,返回值,数据库统一
  time-format: HH:mm # 系统时间格式,接口参数,返回值,数据库统一
  page-size: 80 # 前端默认分页大小
  debug: true # 系统是否处于调试模式
  multiDb: true # 支持多数据库
  dataSource: # 数据库源
    host: ${spring.profiles.host} # 服务器IP
    port: 5432 # 端口号
    class-type: org.postgresql.Driver # 数据库类型 org.postgresql.Driver是postgresql,com.mysql.cj.jdbc.Driver是mysql
    username: odoo16 # 账户
    password: odoo16 # 密码
    max-pool-size: 200 # 连接池大小
    min-idle: 10 
    connection-timeout: 20000
    idle-timeout: 25000
    max-lifetime: 30000

redis: # redis
  config:
    - key: redis-0 # 多源redis标志 一般一个不修改
      hostName: ${spring.profiles.host} # IP
      port: 6379 
      password: 
      database: 
        - 0 # 第0个 对应 RedisDataBase0 类
        - 1 # 第1个 对应 RedisDataBase1类
# nacos配置
spring:
  cloud:
    nacos:
      discovery:
        group: dev
        username: nacos
        password: nacos
        server-addr: ${spring.profiles.host}:8848
  jackson:
    date-format: ${application.datetime-format}
    time-zone: GMT+8

# 消息队列配置
pulsar:
  url: pulsar://${spring.profiles.host}:6650
  enable: false

logging:
  config: classpath:logback-spring-dev.xml

avalon-file文件服务器

application.yml配置项

server:
  port: 8091
  servlet:
    context-path: /file
spring:
  profiles:
    active: dev,file-dev
  application:
    name: avalon-file
  servlet:
    multipart:
      enabled: true
      max-file-size: 200MB  # 上传文件大小
      max-request-size: 200MB 

application-file-dev.yml配置项

spring:
  profiles:
    host: localhost

application:
  multiDb: false  # 不支持多数据库
  cache-type: file  # file 本地文件存储,minio minio存储

pulsar:
  url: pulsar://${spring.profiles.host}:6650
  enable: false

# 本地文件存储配置
file:
  file: ./data/  #本地目录 支持相对路径与绝对路径
  video: ./video/ # 视频存放路径
  image: ./image/ # 图片存放路径
  mode: date # 文件路径生成方式 date 日期方式 存储位置 {db}/YYYY/MM/{UUID}.ext,randon随机存储位置 {db}/{0...255}/{0...255}/{{uuid}}.exit

# minio 存储配置
minio:
  endpoint: http://localhost:9000
  accessKey: minioadmin
  secretKey: minioadmin
  mode: date

HTTP接口

通用文件上传与与下载接口

host:http://localhost:8091/file

上传文件

接口:/file/upload

方式:POST

传参方式:form-data

参数:

{
    "file":File
}

返回值:

{
    "mine": "image/png",
    "size": 3101421,
    "url": "/file/down/pet/2025/05/a4b57c788727474eae13734acbd0f7b3.png",
    "originName": "color4bg_2025-05-12 14_14_25.png"
}

上传图片文件

接口:/image/upload

方式:POST

传参方式:form-data

参数:

{
    "file":File
}

返回值:

{
    "mine": "image/png",
    "size": 3101421,
    "url": "/file/down/pet/2025/05/a4b57c788727474eae13734acbd0f7b3.png",
    "originName": "color4bg_2025-05-12 14_14_25.png"
}

上传视频文件

接口:/video/upload

方式:POST

传参方式:form-data

参数:

{
    "file":File
}

返回值:

{
    "mine": "image/png",
    "size": 3101421,
    "url": "/file/down/pet/2025/05/a4b57c788727474eae13734acbd0f7b3.png",
    "originName": "color4bg_2025-05-12 14_14_25.png"
}

下载图片

地址:http://localhost:8091/file+返回值.url

web

Form表单属性

控制create,保存按钮隐藏

 <form create="false" edit="false">
     ...
</form>

avalon-im 服务器

用于IM通讯,可以单独部署,不依赖avalon-erp

配置文件

application.yml

server:
  port: 8093
  servlet:
    context-path: /im
spring:
  profiles:
    active: dev,im-dev
  application:
    name: avalon-im
  cloud:
    nacos:
      discovery:
        username: nacos
        password: nacos
        server-addr: ${spring.profiles.host}:8848
im:
  wss: false 不启用

application-im-dev.yml

application:
  multiDb: false
  dataSource:
    database: im
    username: avalon
    password: avalon
spring:
  profiles:
    host: localhost

数据库文件

需要自己进行安装

image-20250514104430482

HTTP接口

http-host:http://localhost:8093/im

注册用户

接口:/user/register

method:POST

参数:

{
	company: "公司名字,可以随便填",
	app: "app名称,可以随便填",
	thirdUserId: "app下的用户唯一标识"
}

返回值:

{
	userId: 5 // 文件
}

注册用户

接口:/user/register

method:POST

参数:

{
	company: "公司名字,可以随便填",
	app: "app名称,可以随便填",
	thirdUserId: "app下的用户唯一标识"
}

返回值:

{
	userId: 5 // 文件
}

创建群组

接口:/team/add

method:POST

参数:

{
	"name": "群名字"
}

返回值:

{
	id: 5 // 群id
}

修改群组

接口:/team/update

method:POST

参数:

{
	"name": "修改群名字"
}

返回值:

删除群组

接口:/team/delete

method:POST

参数:

{
	"teamId": 1
}

返回值:

分页获取用户消息

请求,返回值小于pageSize说明是最后一页了

接口:/message/user/get/page

method:POST

参数:

{
    "userId": 3,
    "pageNum": 1,
    "pageSize": 10
}

返回值:

[
    {
        "msgType": "Text",//消息类型,Text文本,Image图片,
        "fromUserId": 2,// 发送用户id
        "isRead": false,
        "eventType": null,
        "toUserId": 3,//目的用户id
        "content": "消息内容",//消息内容
        "stateEnum": "Client",
        "teamId": null,
        "name": null,
        "serverSendTime": null,
        "id": 1267898438633263104, // id 
        "chatType": "Single",
        "timestamp": 1722332144206 //
    }
]

获取离线消息

接口:/message/offline

method:POST

参数:

{
    "userId": 3
}

返回值:

[
    {
        "msgType": "Text",
        "fromUserId": 2,
        "isRead": false,
        "eventType": null,
        "toUserId": 3,
        "content": "消息内容",
        "stateEnum": "Server",
        "teamId": null,
        "name": null,
        "serverSendTime": null,
        "id": 1267905873892741120,
        "chatType": "Single",
        "timestamp": 1722333916896
    }
]

获取消息id

接口:/message/get/id

method:POST

参数:

{
}

返回值:

{
	"id":1268242042090295296
}

清空消息列表的未读消息

接口:/user/chat/clear/unread

method:POST

参数:

{
    "id":2
}

返回值:

{
}

获取聊天列表

接口:/user/chat/list

method:POST

参数:

{
    "id":2
}

返回值:

[
    {
        "top": false, // 置顶
        "lastMsgId": { // 最后一条消息
            "msgType": "Image",
            "id": 1268572304611348480,
            "content": "/file/down/123123.png",
            "timestamp": 1722492806373
        },
        "createTime": "2024-08-01 13:29:50",
        "fromUserId": 2, // 发送方
        "teamId": null,
        "updateTime": "2024-08-01 14:13:26",
        "id": 2,
        "unReadCount": 2, // 未读消息数
        "toUserId": 3,
        "chatType": "Single"
    }
]

ws 连接

发送消息的流程

建立连接->鉴权->发送消息->服务器回发ack确认消息

建立连接 ws地址

ws://localhost:6666/ws

鉴权消息

{
    "msgType":"Auth",
    "fromUserId":3,
    "content": "token" // token 是 唯一标识,可以自定义
}

发送单聊文本消息

{
    "id":1268242042090295296,
    "fromUserId":3,
    "toUserId":2,
    "chatType":"Single", // 单聊
    "msgType":"Text", 
    "content": "Hello,World" 
}

服务器返回单聊文本ACK消息

{
    "id":1268242042090295296
    "chatType":"Single",
    "msgType":"Ack",
    "content": "{'srcId': 1268242042090295295}" // srcId 是确认收到消息的id
}

接受消息的流程

建立连接->鉴权->接受消息->向服务器发送ack确认消息

鉴权消息

{
    "msgType":"Auth",
    "fromUserId":3,
    "content": "{token}"
}

接受单聊文本消息

{
    "id":1268242042090295296,
    "fromUserId":3,
    "toUserId":2,
    "chatType":"Single",
    "msgType":"Text", // Text 文本, Image 图片
    "content": "Hello,World" 
}

向服务器发送单聊文本ack消息

{
    "id":1268242042090295297,
    "chatType":"Single",
    "msgType":"Ack",
    "content": "{'srcId': 1268242042090295296}" // srcId 是确认收到消息的id
}

视图继承

主要字段inheritId 设置被继承的资源id

资源id:模型名.{id}

tree继承

   <record id="hr_user_view_tree" service="base.action.view">
        <field name="name">hr_user_view_tree</field>
        <field name="label">用户</field>
        <field name="viewMode">tree</field>
        <field name="inheritId" ref="base.base_user_view_tree"/>
        <field name="ref_serviceId">base.user</field>
        <field name="arch" type="xml">
            <xpath expr="//field[@name='name']" position="after">
                <field name="staffId"/>
            </xpath>
        </field>
    </record>

form继承

    <record id="hr_user_view_form" service="base.action.view">
        <field name="name">base_user_view_form</field>
        <field name="label">用户表单</field>
        <field name="viewMode">form</field>
        <field name="ref_serviceId">base.user</field>
        <field name="inheritId" ref="base.base_user_view_form"/>
        <field name="arch" type="xml">
            <xpath expr="//row" position="after">
                <notebook>
                    <page label="基本信息">
                        <row>
                            <col>
                                <field name="staffId"/>
                            </col>
                        </row>
                    </page>
                </notebook>
            </xpath>
        </field>
    </record>

用户教程

权限设置

入口

image-20250515121847804

权限组

拥有规则,菜单,模型等权限的组合,用户可以属于多个权限组

增加用户

被增加的用户拥有当前权限组的所有权限

image-20250515122148396

增加规则

设置模型记录的访问条件,创建规则时,使用的是查询字符串,内部支持的变量,有userId,表示当前用户

image-20250515124018909

增加菜单

用户可以访问的菜单,但不能表示用户可以访问菜单对应的模型,方法等,需要保证菜单所有执行的权限足够。正常情况下主要设置模型足够满足要求。

image-20250515124231907

增加模型

用户对模型有访问,修改,新增,删除的权限,有访问,则对应的菜单会显示。

image-20250515124425813

快捷键

Shift+Alt+H:跳转到Excalidraw绘画页面

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published