odoo java版 前后分离快速开发平台,基于开源技术栈精心打造,融合Vue3+SpringBoot。 适配数据库mysql,postgres 支持标准的RAID权限功能,
前端介绍: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. 拉取 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
# 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
# 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
docker exec -it postgres-container psql -U postgres
CREATE ROLE avalon WITH LOGIN CREATEDB PASSWORD 'avalon';
# 验证角色创建
\du
教程:https://docs.pingcode.com/baike/2874339
username改为avalon,password改为avalon
一般情况下不用修改
找到对应的application.yml文件,修改nacos对应的username与password,默认情况下不用修改,如果有修改avalon-file与avalon-im项目相同的文件,进行一样的调整
启动时需要增加如下参数,否则会报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
安装方法:https://blog.csdn.net/qq_37523550/article/details/136183405
执行yarn install 安装依赖
登录界面如下
前端分离
后端采用微服务架构,
模块高内聚,低耦合方式,可继承的开发方式,大大提高开发效率,
支持mysql/postgres多数据库开发连接
目录结构图
以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上创建一个属于自己的模块,此模块功能有房租出租功能
在avalon-erp/src/main/java/com/avalon/erp/addon包下创建house包
在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;
}
}
在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 "房屋";// 标题
}
}
在house包下,创建resource/view包
在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",
};
}
}
使用IDEA,webstorm启动项目
项目启动后,在登录页面,点击 管理数据库 按钮
点击创建数据库按钮,输入demo,点击确认,等待一段时间,数据库会自动创建完毕
数据库创建之后,点击数据库名字,会跳转到登录页面,
默认管理员账户admin,密码是123456
如果模块没有显示,则点击App菜单下的更新模块菜单,进行刷新模块,刷新后按F5刷新当前页面
点击安装即可
恭喜已经完成house模块的开发
- 基本字段
BigDecimal,
BigInteger,
Boolean,
Date,
DateTime,
Double,
Float,
Html,
Image,
Integer,
Selection(Enum),
String,
Text,
Time,
Password
- 关联字段
One2one, 1对1
One2many, 1对多
Many2one,多对1
Many2many 多对多
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();
}
public Integer getMax() // 最大值
public Integer getMin() //最小值
public Integer getMaxLength() // 最大长度
public Integer getMinLength() // 最小长度
//模块名
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()
/**
* 查询主键集合
*
* @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("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;
}
结果:
/**
* 创建数据库表
*/
public void createTable()
/*
删除数据库表
*/
public void dropTable()
/*
数据库表是否存在
*/
public Boolean existTable()
/*
数据库字段是否存在
*/
public Boolean existField(Field field)
/*
删除数据库字段
*/
public void dropField(String fieldName)
/**
* 升级数据表结构
*/
public void upgradeTable()
用于记录临时记录,不会生成数据库结构
主要用于,用于定制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
字段)指向另一个模型来实现的继承方式。
- 模型独立性:委托继承的子模型不会直接继承父模型的字段和方法,而是通过外键(
Many2one
)字段关联到父模型。 - 不修改父模型:父模型保持独立,子模型通过外键引用父模型来获得其字段和方法。
- 数据分离:父模型和子模型的数据分别存储在各自的数据库表中,通过外键关联。
- 适用场景:当你需要逻辑上关联两个模型,但不希望直接修改或扩展父模型时,可以使用委托继承。
相关接口
/**
* 委托继承
* 格式 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>
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>
例子
在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 前端会提示操作成功
}
}
前端未选择记录效果:
前端选择记录效果:
支持在tree视图的表格内修改,新增记录,而不用弹窗,这样的操作方式,适用于字段比较少的表
效果:
用法:
<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 视图 是用于显示单条记录的详细信息的视图类型。它是 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>
例子
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>
例子
将模型分为左右两部分,左边是树状列表,右边是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>
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>
效果如下:
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>
页面效果:
创建前端菜单入口,只能三级
<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>
按钮默认都是调用后台方法 调用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;
}
可以在数据库模型,创建时,在模型表中增加记录
介绍创建模型的一般方式
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>
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": "部门"
}
接口:/service/export/{serviceName}/excel
方式:POST
参数:serviceName=base.user
{
"order": "",
"field": "id,account,name",
"condition": "('id',in,3,1)"
}
返回值: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 // 成功导入的个数
}
"('field',=,2|'a'|1.1)"
"('field',between,1,2)"
"('field',in,1,2,3)"
"!('field',=,2|'a'|1.1)"
"('field',=,2)&('field',=,2)"
"('field',=,2)|('field',=,2)"
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
spring:
profiles:
host: localhost
pulsar:
url: pulsar://${spring.profiles.host}:6650
enable: false
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
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
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
通用文件上传与与下载接口
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
控制create,保存按钮隐藏
<form create="false" edit="false">
...
</form>
用于IM通讯,可以单独部署,不依赖avalon-erp
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:
multiDb: false
dataSource:
database: im
username: avalon
password: avalon
spring:
profiles:
host: localhost
需要自己进行安装
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
}
]
接口:/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"
}
]
建立连接->鉴权->发送消息->服务器回发ack确认消息
ws://localhost:6666/ws
{
"msgType":"Auth",
"fromUserId":3,
"content": "token" // token 是 唯一标识,可以自定义
}
{
"id":1268242042090295296,
"fromUserId":3,
"toUserId":2,
"chatType":"Single", // 单聊
"msgType":"Text",
"content": "Hello,World"
}
{
"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"
}
{
"id":1268242042090295297,
"chatType":"Single",
"msgType":"Ack",
"content": "{'srcId': 1268242042090295296}" // srcId 是确认收到消息的id
}
主要字段inheritId 设置被继承的资源id
资源id:模型名.{id}
<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>
<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>
拥有规则,菜单,模型等权限的组合,用户可以属于多个权限组
被增加的用户拥有当前权限组的所有权限
设置模型记录的访问条件,创建规则时,使用的是查询字符串,内部支持的变量,有userId,表示当前用户
用户可以访问的菜单,但不能表示用户可以访问菜单对应的模型,方法等,需要保证菜单所有执行的权限足够。正常情况下主要设置模型足够满足要求。
用户对模型有访问,修改,新增,删除的权限,有访问,则对应的菜单会显示。
Shift+Alt+H:跳转到Excalidraw绘画页面