E10 开发培训
学习资料获取
可访问我的个人网站获取泛微开发的学习知识,地址:yaolilin.com
访问我的开源主页,可查看泛微的开发知识,代码工具,和泛微可复用应用:
- github: YaoLilin (姚礼林)
- gitee : 姚礼林 (yaolilin) - Gitee.com
泛微官方学习资料:E10开发指引
前端
E10 前端架构
框架:React 17 组件:泛微自研组件
E-Code 平台介绍
E-code 平台的功能和 E9 的 E-Code 平台差不多,它是一个在线的前端代码编辑器,可编写 React 组件以及注册事件,可实现修改标准页面的组件和数据,以及创建自定义页面、进行接口拦截等。
E-Code 页面进入链接: /ecode
进入页面后是一个项目列表,你可以在这里创建项目

进入项目后就可以在此页面创建 E-Code 应用,发布该应用后就可以让该应用生效。
组件开发
实现效果:在前端页面显示创建的组件

创建 E-Code 应用
创建应用选择 React 类型,创建后应用下会自动创建 entry.js 文件,相当于 E9 的 register.js 文件,它是一个前置加载文件,意味着它会在页面加载前执行,可以在此文件上注册组件参数修改、组件替换、接口拦截、新页面注册等。
创建组件
创建一个父组件和一个子组件,可创建 components 文件夹,在里面存放组件。

组件的编写与常规 React 应用的编写一致,这点与 E9 不同,E10 的组件导入和导出也是用标准的 JS 模块导入导出方法。
**子组件 childCom.js** 使用 `export default` 进行组件导出,这样可以在本应用内获取到本组件
export default function (){
return <div>子组件</div>
}
**父组件 index.js** 使用 `import` 导入子组件,与 React 写法相同
import ChildCom from './childCom';
export default function(){
return <div>
父组件
<ChildCom />
</div>
}
将组件添加到前端页面
将创建的组件渲染到前端
entry.js 是一个前置加载文件,会在页面加载前执行,可以在 entry.js 内获刚才创建的组件,然后渲染到页面指定元素。
要在 entry.js 文件获取创建的组件,必需使用异步方式获取组件,也就是使用 `asyncImport` 进行异步导入。
const MyCom = lazy(() => asyncImport('${appId}', 'component/index'));
异步获取的组件必需放在 `Suspense` 组件中,此组件的作用是等待子组件加载完成,加载完成后显示子组件,在未加载完成时显示 `fallback` 中的组件。
<Suspense fallback={<span>waiting</span>}>
<div style={style}>
<MyCom />
</div>
</Suspense>
最后使用 `render(<App />, root);` 将组件渲染到指定页面元素。
entry.js
import {render} from "react-dom";
import { useState, Suspense, lazy } from 'react';
import { asyncImport } from '@weapp/ecodesdk';
// 创建 App 挂载元素
const root = document.createElement('div');
document.body.appendChild(root);
// 样式处理,也可以新建 css 文件
const style = {
position: 'absolute',
zIndex: 9999,
top: 100,
left: '50%',
transform: 'translate(-50%, 0)',
fontSize: 60,
fontWeight: 600,
backgroundColor: 'rgba(255, 255, 255, 0.8)',
boxShadow: '0 0 20px rgba(0, 0, 0, 0.5)'
};
const MyCom = lazy(() => asyncImport('${appId}', 'component/index'));
function App () {
return (
<Suspense fallback={<span>waiting</span>}>
<div style={style}>
<MyCom />
</div>
</Suspense>
);
}
render(<App />, root);
E10 组件库介绍
E10 的前端页面大多数都由组件库中的组件构成。 E10 组件库地址:/ui
在 E-Code 中也可以看到组件库

组件参数复写
**应用场景:**
- 适合只通过修改组件参数就能完成对组件修改的场景
- 只是通过组件参数复写作为执行入口,判断当前页面是否已经加载
通过修改标准页面上的组件参数,就可以达到修改组件数据的效果。
示例:通过修改 `Title` 组件参数,在流程表单添加按钮

先创建自定义按钮
import {Button} from '@weapp/ui';
const CustomButton = () => {
return <Button>自定义按钮</Button>
}
export default CustomButton;
然后在 `entry.js` 文件注册组件参数修改事件,使用 `regOvProps` 进行注册,参数说明:
- 所属组件库:pc 端组件的组件库一般为 weappUi
- 组件名称:需要进行参数修改的组件名称
- 参数修改函数:可在此函数对参数进行修改
通过异步方式导入 `CustomButton` 组件,然后将此自定义按钮组件添加到 `Title` 组件的 `buttons` 参数中。
注意:进行组件参数修改时需要先判断页面地址,和根据组件的标识(weId)判断是否为对应组件,一般可通过该标识的最后几个字母进行判断,如:_6xdqsn 如何获取 weId:在组件参数中获取,通过 React 浏览器插件就能方便的查看组件参数,看到 weId
import {asyncImport} from '@weapp/ecodesdk';
import {regOvProps} from '@weapp/utils';
import {lazy, Suspense} from 'react';
const CustomButton = lazy(() => asyncImport('${appId}', 'components/CustomButton'));
/**
* 在流程中添加自定义按钮
*/
regOvProps('weappUi','Title',(props)=>{
try {
if(props.weId.endsWith('_6xdqsn') && location.href.includes('/sp/workflow/flowpage/view')){
const {buttons} = props;
if(buttons && buttons.length > 0){
buttons.splice(buttons.length - 1, 0,
<Suspense fallback={<span></span>}>
<CustomButton/>
</Suspense>
)
}
}
} catch (e) {
console.error('添加自定义按钮出现异常', e);
}
return props;
},1);
示例应用下载:
组件替换
**应用场景:** 需要在原组件添加其它组件,实现更复杂的修改
示例:通过替换 `Menu` 组件,在门户添加一个菜单

**创建自定义组件**
此组件将会作为替换的 Menu 组件
组件接受两个参数:
- props:原组件参数
- ref:组件引用
import { Menu } from '@weapp/ui';
const newMenu =(props,ref)=>{
const { OriginCom } = props;
// 添加新菜单
const data = props.data;
data.push({ id: 'custom-tab', content: '新添加的菜单' })
// 可以使用别的组件,也可以用 <OriginCom /> 使用原组件
// 使用原来的组件参数
return(
<Menu {...props} data={data} noOverwrite={true} ref={ref}/>
)
}
export default newMenu;
**注册组件替换事件**
在 `entry.js` 文件中使用 `regOvComponent` 注册组件重写事件
⚠️ 注意:一定要在组件加上 _noOverwrite 标识,标记组件已被替换,并在组件替换时判断该标识,防止出现无限循环导致页面卡死
import React from 'react';
import { regOvComponent } from '@weapp/utils';
import { asyncImport } from '@weapp/ecodesdk';
const NewMenu = React.lazy(() => asyncImport('${appId}', 'NewMenu'));
regOvComponent('weappUi', 'Menu', (Com) => {
return React.forwardRef((props, ref) => {
// 需要根据 weId 去判断是否是要替换的组件,根据 noOverwrite 判断是否可重写,否则会无限循环造成卡死
if (location.href.includes('/portal/view') && props?.weId?.endsWith('_39u13z')
&& !props._noOverwrite) {
return (
<React.Suspense fallback={() => { }}>
<NewMenu ref={ref} {...props} OriginCom={Com} />
</React.Suspense>
)
}
// 如果不是要替换的组件需要返回原组件
return <Com ref={ref} {...props} />
});
}, 1)
E-code 示例应用下载:
如何知道页面是使用哪个组件
方法一:通过样式名判断
通过浏览器的开发者工具,查看该组件的样式名, 比如下面可知道这个组件为 menu 组件。 此方法的缺点是不太直观

方法二:使用 React 浏览器插件(推荐)
该插件可通过插件商店下载,建议所有开发人员都使用此插件。 通过此插件可以很直观的看到组件名称和它所属的组件库,还有组件的参数。

接口拦截
**应用场景:** 通过拦截接口的返回数据,达到修改页面数据的目的
说明文档:e-code二开请求和拦截说明文档
**所有接口拦截,都需要写在 entry.js 文件上**
使用官方工具进行拦截(首选)
接口返回拦截
使用 `devRequest` 对接口进行拦截 注意:此方法不一定有效,只允许拦截部分接口,如果不成功就换其它方法
// 请求工具方法
import { devRequest } from '@weapp/ecodesdk';
// 使用方式同 request, 参数保持一致
// get 调用
devRequest({ url: '/papi/secondev/health' }).then(res => {
if (res.code === 200) {
Dialog.message({ type: 'info', content: `请求成功:${dayjs().unix()}` })
} else {
Dialog.message({ type: 'info', content: `请求失败:${dayjs().unix()}` })
}
}).catch(err => {
Dialog.message({ type: 'info', content: `请求异常:${dayjs().unix()}` })
console.error(`请求异常:${dayjs().unix()}`, err)
})
// post 调用
devRequest({ url: '/papi/secondev/health', method: 'post' }).then(res => {
if (res.code === 200) {
Dialog.message({ type: 'info', content: `请求成功:${dayjs().unix()}` })
} else {
Dialog.message({ type: 'info', content: `请求失败:${dayjs().unix()}` })
}
}).catch(err => {
Dialog.message({ type: 'info', content: `请求异常:${dayjs().unix()}` })
console.error(`请求异常:${dayjs().unix()}`, err)
})
使用 axios 进行拦截
`axios` 是一个 js 网络请求库,它是通用的,不受泛微的限制,所以它可以拦截所有接口的请求。
请求拦截
在接口请求时添加请求参数
示例:拦截 `api/hrm/employee/getTopMenu` 接口,添加请求参数
import axios from 'axios';
// 前置拦截,获取请求数据
axios.interceptors.request.use(
config =>{
// config 包含所有请求信息
const {url,params} = config;
if(url.includes('api/hrm/employee/getTopMenu')){
// 添加参数
params.isTest = 1;
}
return config;
},
error =>{
// 对请求错误做些什么
return Promise.reject(error);
}
)
响应拦截
示例:拦截 `api/hrm/employee/getTopMenu` 接口,添加按钮数据
import axios from 'axios';
// 后置拦截,获取接口返回数据
axios.interceptors.response.use(
response =>{
// response 里包含了响应信息,以及 config,使用 config 可以获取请求信息,data可以获取响应数据
const {data,config:{url,params}} = response;
if(url.includes('api/hrm/employee/getTopMenu')){
// 能够获取到上面请求拦截中添加的参数
const {isTest} = params;
// 修改返回数据,添加一个按钮
if(data.data){
data.data.push({
content:'新增按钮',
id:'devBt'
});
}
}
return response;
},
error =>{
// 对请求错误做些什么
return Promise.reject(error);
}
)
效果: 添加了一个按钮

示例下载
ecode 应用
配置文件
在开发中我们常遇到,在哪条流程生效,在哪个节点生效,那这些就需要配到配置文件里面。
配置文件会在 E-Code 应用自动创建,路径为:`config/config.js`
配置文件编写示例:
const config = {
workflowId:[1243,34356]
}
export default config;
获取配置文件:
使用`import`导入配置文件,注意 from 后面的路径,是相对于当前文件的路径,当前文件路径是根目录,所以使用 ./ 表示当前路径。
配置文件是前置加载的,在 entry.js 文件无需异步导入。
注意:配置文件导入时 config 不要加括号,不然会获取不到配置文件
import config from './config/config'
// 使用配置文件
config.width;
配置文件使用示例
配置文件内容:
/**
* 下载PDF按钮相关配置
*/
const config = {
// 需要下载的文件字段名称,可多个,只要表单中有这些字段(需放在表单),则显示下载PDF按钮
fileFieldNames:['fj_fltf','fj_4ch3']
}
export default config;
在 entry.js 文件进行组件参数修改,实现在流程表单中添加按钮,根据配置文件,判断当前流程是否包含指定字段,如果有才添加按钮
import {asyncImport} from '@weapp/ecodesdk';
import {regOvProps} from '@weapp/utils';
import {lazy, Suspense} from 'react';
import config from './config/config';
function enable(){
try{
if(!config) return false;
const fileFieldNames = config.fileFieldNames;
if(!fileFieldNames || fileFieldNames.length === 0) return false;
const formSdk = window.WeFormSDK?.getWeFormInstance();
if(!formSdk) return false;
// 验证配置文件中的字段名能否成功转换为字段ID
return fileFieldNames.some(name => {
const id = formSdk.convertFieldNameToId(name);
return typeof id === 'string' && id.length > 0;
});
}catch(e){
console.warn('enable() 出现异常',e);
}
}
/**
* 在流程中添加下载PDF按钮
*/
regOvProps('weappUi','Title',(props)=>{
try {
if(props.weId.endsWith('_6xdqsn') && location.href.includes('/sp/workflow/flowpage/view')
&& enable()){
const {buttons} = props;
if(buttons && buttons.length > 0 && formData){
buttons.splice(buttons.length - 1, 0,
<Suspense fallback={<span></span>}>
<DownloadButton formData={formData}/>
</Suspense>
)
}
}
} catch (e) {
console.error('注入下载按钮出现异常', e);
}
return props;
},1);
新页面开发
**场景:** 通过开发创建新页面,比如纯开发的门户,报表页面
创建新页面中的组件
const NewPage = ()=>{
return <div>新页面</div>
}
export default NewPage;
注册新页面
在 `entry.js` 文件使用 `regOvProps` 注册新页面
**创建路由组件** 需要使用 `Route` 组件来创建路由组件,在组件内放新页面的组件(这里为 NewPage 组件)。 path 为新页面的地址,地址一般为:/sp/custom/{appId}/{页面组件名称} 。 通过 `publicUrl` 获取地址前缀,不过一般都是为空的。
const { publicUrl } = appInfo('@weapp/ecodesdk');
// 创建路由组件
const newPage = (
// publicUrl可能为空,前缀必需包含 /sp/custom/
<Route path={`${publicUrl}/sp/custom/` + '${appId}/newPage'}>
<React.Suspense fallback= {() => { }}>
<NewPage />
</React.Suspense>
</Route>
);
**注册路由** 下面除了 `newPage` , 其它都为固定写法
// 注册路由
regOvProps('weappEcodesdk', 'Switch', (props) => {
regReactChildren(props, newPage);
return props;
}, 0);
**完整示例**
import React from 'react';
import { Route } from 'react-router-dom';
import { regOvProps, appInfo, regReactChildren } from '@weapp/utils';
import { asyncImport, jsonp } from '@weapp/ecodesdk';
const NewPage = React.lazy(() => asyncImport('${appId}', 'newPage'));
const { publicUrl } = appInfo('@weapp/ecodesdk');
// 页面地址:/sp/custom/993285932735250435/newPage
// 创建路由组件
const newPage = (
// publicUrl可能为空,前缀必需包含 /sp/custom/
<Route path={`${publicUrl}/sp/custom/` + '${appId}/newPage'}>
<React.Suspense fallback= {() => { }}>
<NewPage />
</React.Suspense>
</Route>
);
// 注册路由
regOvProps('weappEcodesdk', 'Switch', (props) => {
regReactChildren(props, newPage);
return props;
}, 0);
实现效果:

示例应用:
加载第三方 js 库
表单 JS 开发
表单 SDK
E10 的流程表单 SDK 与 E9 的表单 Js API 使用方法差不多。 入口:在布局页面,点击此图标

在流程中使用 E-Code 应用
在流程高级设置中可以添加 E-Code 应用,可以选择创建新的应用或者关联已有应用,不过要注意,如果选择创建新的应用,则这个应用只能用在这一条流程上,你可以到 E-Code 页面先创建应用,然后再关联应用,这样就可以在所有流程都可以用这个 E-Code 应用。


流程的 E-Code 应用放在此项目下,必需勾选系统项目才能显示此项目。流程只能关联此项目中的 E-Code 应用,你可以在此项目下创建流程的 E-Code 应用。

当进入表单后,会自动执行关联的 E-Code 应用下的非前置加载文件,因此在应用下的 js 文件中正常写代码即可,例如写在应用下的 index.js 文件
前端开发规范
官方文档说明:二次开发规范说明文档
组件必需通过样式名添加二开标记
任何通过二开在页面上添加元素的组件(比如弹窗、按钮、表格等),都必需在组件的样式名上注明二开标记,如:“ecode 应用id,代码入口(是ecode还是 表单js)”,方便定位二开代码编写位置。
这个源自之前的经历,我经常碰到这个按钮不知道是在哪里开发添加的,找的很痛苦
示例:
- appid-${appId} 为ecode 应用id
- seconddev-ecode 表示这是在 ecode 二开添加的
<Button onClick={handleClick} className={'appid-${appId} seconddev-ecode'} >下载为PDF</Button>
每个 E-Code 应用必需添加说明文档
在每个 E-Code 应用添加 README.md 文件,说明作者信息,开发的内容,如何配置等,方便了解这个应用的功能是什么,方便后续维护。
文档不齐全,维护两行泪
示例:
# EB列表单元格合并开发说明
作者:泛微柳州 姚礼林
日期:2026-02-26
## 实现功能
e-builder 表格可合并重复数据的单元格,如果表格中的字段存在连续重复,则进行合并。
- 可配置合并依据字段,如果该字段存在连续重复,则合并这些单元格
- 可配置其它需要合并的字段,在合并依据字段合并时,如果这些字段的值也相同,则进行合并
- 可对每个 e-builder 表格配置合并
合并前:

合并后:

## 配置说明
需修改 `config/config.js` 文件,需配置需要合并的eb表格,在配置中的表格才进行合并。
配置示例:
const config = { viewConfigs :[ { // 列表id,通过链接地址获取,例如链接包含: // /sp/ebdapp/build/1211771683381723136/search/1211771941323030537-7516533764297595633? // 则取 1211771941323030537 id:'1240398310603153522', // 主键字段名,必填,按此字段合并单元格,值相同且上下连续时合并 primaryKeyField:'supplier', // 其它需要合并单元格的字段名,可为空 mergeColFieldNames:['produce'] }, { // 其它表格配置 } ] }
export default config;
在 `viewConfigs` 中可配置多个表格
后端
架构说明
| 层级 | 技术组件 | 说明 | | ------- | ------------------------------------------------- | ---------------- | | 注册/配置中心 | Nacos(支持 Zookeeper/Consul/Eureka 一键切换) | 服务注册发现、动态配置管理 | | 网关层 | Spring Cloud Gateway | 统一入口、负载均衡、熔断降级限流 | | 服务间调用 | Dubbo + OpenFeign | RPC + HTTP 双通道 | | 分布式事务 | Seata | 保障跨服务数据一致性 | | 消息队列 | RabbitMQ / ActiveMQ / RocketMQ(可一键切换) | 异步解耦 | | 缓存 | Redis(单机/哨兵/集群模式) | 高性能缓存 | | 链路追踪 | SkyWalking | 全链路性能监控 | | 日志收集 | Filebeat → Logstash → ElasticSearch → Kibana(ELK) | 集中日志分析 | 框架:使用 SpringBoot + Spring Cloud 微服务,支持单体部署和微服务部署(大多数都为单体部署) ⚠️ 注意:需要区分单体和微服务环境,这会对开发产生影响,如果是微服务环境则一些内部接口会无法调用,需要使用 RPC 接口
配置管理:配置文件 或 Nacos 最低内存要求:32G Java 版本:8
**二开服务** E10 会有一个独立的二开服务,专门用来做二次开发,它和主服务是分开的,我们写的二次开发都会部署到二开服务下。二开服务可以单独重启,不会导致主服务停止运行。

开发环境搭建
创建 Gradle 项目
E10 开发环境搭建要比 E9 复杂的多,你需要创建一个 Gradle 项目,可以使用 IDEA 进行创建,创建时 JDK 选择 1.8

创建后会下载 Gradle ,由于网络环境可能会下载失败
创建好项目之后,创建 common 子模块(你可能不需要,但为了兼容下面的配置)以及另外一个模块,你开发的代码就写在这个模块上。
然后在 build.gradle 文件覆盖粘贴下面代码:
group 'com.weaver.seconddev'
version '1.0.0'
description 'e10二开'
// 应用到所有项目(包括根项目)
allprojects {
apply plugin: 'java'
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
compileJava {
options.encoding = 'UTF-8'
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_1_8
}
repositories {
maven { url 'https://maven.aliyun.com/repository/public/' }
mavenLocal()
mavenCentral()
}
// 添加 zipJar 任务,生成 build.zip 文件,可以用于上传到二开服务
tasks.register('zipJar', Zip) {
dependsOn jar
archiveFileName = 'build.zip'
destinationDirectory = layout.buildDirectory.dir("libs")
from jar.archiveFile
}
jar {
// 确保在执行jar任务之前先执行classes任务
dependsOn ':classes'
// 执行 jar 任务之后执行 zipJar 任务,生成 build.zip 文件
finalizedBy(zipJar)
}
}
// 应用到子模块
subprojects { subproject ->
// 除 common 模块外,其它子模块都引入 common 模块的源码
if (subproject.name != 'common') {
dependencies {
implementation project(':common')
}
}
// 打包配置
if (subproject.name != 'common') {
jar {
archiveBaseName = "secondev-hnweaver-" + subproject.name
}
}
sourceSets {
main {
resources {
exclude '**'
}
} }
jar {
manifest {
attributes 'weaver-ecode-seconddev-id': 'hnweaver-' + rootProject.version,
'Implementation-Version': rootProject.version,
'Implementation-Vendor-Id': rootProject.group,
'Implementation-Title': rootProject.name
}
// 将源码打进jar包
from(subproject.sourceSets.main.java)
}
// 添加公共的依赖
dependencies {
compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
testImplementation("org.junit.jupiter:junit-jupiter-api:5.13.3")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.13.3")
testImplementation("org.junit.platform:junit-platform-launcher:1.13.3")
testImplementation("org.mockito:mockito-core:5.18.0")
testImplementation("org.mockito:mockito-junit-jupiter:5.18.0")
// 添加二开服务拉取的清单依赖
def includeType = ['**/*.jar', '**/*.class']
implementation fileTree(dir: rootProject.projectDir.getPath() + '/secDevLib', includes: includeType)
// 项目二开自定义依赖
implementation fileTree(dir: subproject.projectDir.getPath() + "/out-dep", includes: includeType)
}
test {
useJUnitPlatform()
}
}
该 Gradle 包含了以下配置:
- java 版本为 1.8
- E10 的依赖都放在 `secDevLib` 此目录下
- 第三方依赖都放在 `out-dep` 目录下
- 所有模块都依赖 common 模块(放公共代码)
- 执行 jar 任务时打成符合 E10 部署要求的压缩包
- 添加测试依赖:包含 junit5 和 mockito
build.gradle 文件中的 hnweaver 为组织名称,你可以改为你的组织名称
拉取开发依赖
按照官方说明文档,在你的 E10 服务器中拉取依赖到你的电脑中,文档:e10平台定制开发指引
将所有依赖都拷贝到项目的 `secDevLib` 目录中,然后重新加载 gradle 项目。
开发环境项目模板
使用此项目模板可免除上面复杂的配置,里面开发环境都帮你搭好了 模板下载:
注意:模板里面的开发依赖比较旧,需要去除里面的依赖,到服务器拉取开发依赖到 `secDevLib` 目录中
你的第一个接口
在你的开发环境项目上创建一个接口,如果你使用的是我给的开发环境模板,那么就有这个接口了
package com.weaver.seconddev.hnweaver.demo.api;
import com.weaver.common.authority.annotation.WeaPermission;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 姚礼林
* @desc 测试接口
* @date 2024/4/1
*/
@RestController
@RequestMapping({"/api/secondev/hnweaver/workflow/demo","/papi/secondev/hnweaver/workflow/demo"})
@WeaPermission(publicPermission = true)
@Slf4j
public class ApiDemoController {
@GetMapping("/hello")
public String hello() {
return "hello world";
}
}
E10 后端 WEB 接口的开发与 SpringBoot 中是相同的。
接口必需包含的注解:
- @RestController:表示此接口只返回数据,而不是视图
- @WeaPermission:不加则访问不了
- @RequestMapping:接口路径,/api 开头表示需要认证才能访问,/papi 开头表示此接口是公共暴露的,不需要进行认证就能访问(相当于 E9 的白名单接口)
打包与部署
执行你的模块中的 gradle 的 jar 任务,生成部署的压缩包

在当前模块的 /build/libs 目录下,找到 build.zip 文件,等会需要将此压缩包上传到系统进行部署

打开你的 E10 系统,进入 /ecode 页面,然后点击此图标进入监控管理平台(必需是系统管理员账号)

选择部署 jar,选择二开服务,上传 build.zip 文件夹,此时会进行文件校验,过程比较久需要耐心等待。之后会让你填写开发说明,以及各个类的说明。

然后进行 jar 包审核,审核通过之后到运维平台重启二开服务

重启之后在系统链接地址上输入 `/api/secondev/hnweaver/workflow/demo/hello` 进行访问,看是否会返回 hello world
团队 jar 包部署
对于团队开发,可以在项目中为每个团队成员创建一个模块,例如 yllDev,在部署时打包当前模块的 jar 包,这样每个人打包的 jar 包就不会冲突。
⚠️ 注意:jar 包之间不能包含相同的文件,否则无法通过部署校验,如果团队中共享了公共模块的代码,需将公共模块进行独立打包部署,各成员的 jar 包不含公共模块代码
WEB 接口开发
接口开与 SpringBoot 的接口开发一致
强制要求: 接口统一返回 `WeaResult` 对象
接口开发示例:
Controller:
package com.weaver.seconddev.hnweaver.demo.api;
import com.weaver.common.authority.annotation.WeaPermission;
import com.weaver.common.base.entity.result.WeaResult;
import com.weaver.seconddev.hnweaver.demo.domain.param.HelloParam;
import com.weaver.seconddev.hnweaver.demo.service.HelloService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* @author 姚礼林
* @desc 测试接口
* @date 2024/4/1
*/
@RestController
@RequestMapping({"/api/secondev/hnweaver/workflow/demo","/papi/secondev/hnweaver/workflow/demo"})
@WeaPermission(publicPermission = true)
@Slf4j
@RequiredArgsConstructor
public class ApiDemoController {
private final HelloService helloService;
@GetMapping("/hello")
public WeaResult<String> hello(@RequestParam("name") String name) {
return WeaResult.success("hello world");
}
@PostMapping("/hello")
public WeaResult<String> posetHello(@RequestBody HelloParam param) {
if (param == null || param.getName() == null) {
return WeaResult.fail("name 不能为空");
}
if (helloService.hello(param)) {
return WeaResult.success("hello world");
}
return WeaResult.fail("执行错误");
}
}
请求参数对象:
package com.weaver.seconddev.hnweaver.demo.domain.param;
import lombok.Data;
/**
* @author 姚礼林
* @desc 请求参数
* @date 2026/6/16
**/@Data
public class HelloParam {
private String name;
private Integer age;
}
业务接口:
package com.weaver.seconddev.hnweaver.demo.service;
import com.weaver.seconddev.hnweaver.demo.domain.param.HelloParam;
/**
* @author 姚礼林
* @desc 测试业务接口
* @date 2026/6/16
**/
public interface HelloService {
boolean hello(HelloParam param);
}
业务实现类:
package com.weaver.seconddev.hnweaver.demo.service.impl;
import com.weaver.seconddev.hnweaver.demo.domain.param.HelloParam;
import com.weaver.seconddev.hnweaver.demo.service.HelloService;
/**
* @author 姚礼林
* @desc 测试业务类
* @date 2026/6/16
**/
public class HelloServiceImpl implements HelloService {
@Override
public boolean hello(HelloParam param) {
return true;
}
}
全局异常处理器
如果接口发生异常,则会使用系统默认的对象进行返回,提示服务器错误,可以创建全局异常处理器,让发生异常时也返回 `WeaResult` ,这样前端能统一按 `WeaResult` 处理。
package com.weaver.seconddev.hnweaver.common.handler;
import com.weaver.common.base.entity.result.WeaResult;
import com.weaver.seconddev.hnweaver.common.exception.ApiExceptionMsgInterface;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器,仅作用于 com.weaver.seconddev.hnweaver 包下的 Controller
*/
@RestControllerAdvice(basePackages = "com.weaver.seconddev.hnweaver")
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public WeaResult<String> handleException(Exception ex) {
log.error("接口发生异常:{}",ex.getMessage(), ex);
if (ex instanceof ApiExceptionMsgInterface) {
return WeaResult.fail("系统异常:" + ex.getMessage());
} else if (ex instanceof MissingServletRequestParameterException) {
return WeaResult.fail("缺少参数:" + ex.getMessage());
}
return WeaResult.fail("系统发生异常");
}
}
配置获取
配置文件路径:`E10部署路径\weaver-secondev-service\webapps\ROOT\WEB-INF\classes\weaver\config\config-center\weaver-secondev-service.properties`
读取示例:
@Data
@Configuration
@RefreshScope
public class TwoHaoHrProperty {
@Value("${2HaoHr.appId}")
private String appId;
@Value("${2HaoHr.appKey}")
private String appKey;
@Value("${oaAddress}")
private String oaAddress;
@Value("${2HaoHr.ssoUrl:https://h5.2haohr.com/browser/authorize.html}")
private String ssoUrl;
}
**读取其它配置文件:**
使用 `PropertySource` 指定配置文件
⚠️ 注意:如果配置文件不存在会导致服务无法启动
@PropertySource("classpath:/weaver/config/config-center/seconddev-demo-openapi.properties")
@Configuration
@Data
public class DemoProperties {
@Value("${openapi.serverAddress}")
private String serverAddress;
@Value("${openapi.cropid}")
private String cropid;
@Value("${openapi.appKey}")
private String appKey;
@Value("${openapi.appSecret}")
private String appSecret;
}
日志
E10 默认日志级别为 error,如果需要输出 info 或 debug 级别日志,需要修改配置文件配置日志级别,修改后只能临时生效,过段时间会自动恢复日志级别为 error。
**配置日志级别方法:** 修改 `E10部署路径\weaver-secondev-service\webapps\ROOT\WEB-INF\classes\weaver\config\config-center\weaver-secondev-service.properties` 配置文件,在配置文件加上:
logging.level.com.weaver.seconddev=[日志级别]
**日志输出:** 在类中使用 `@Slf4j` 注解
示例:
package com.weaver.seconddev.hnweaver.demo.api;
import com.weaver.common.authority.annotation.WeaPermission;
import com.weaver.common.base.entity.result.WeaResult;
import com.weaver.seconddev.hnweaver.demo.domain.param.HelloParam;
import com.weaver.seconddev.hnweaver.demo.service.HelloService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* @author 姚礼林
* @desc 测试接口
* @date 2024/4/1
*/
@RestController
@RequestMapping({"/api/secondev/hnweaver/workflow/demo","/papi/secondev/hnweaver/workflow/demo"})
@WeaPermission(publicPermission = true)
@Slf4j
@RequiredArgsConstructor
public class ApiDemoController {
private final HelloService helloService;
@GetMapping("/hello")
public WeaResult<String> hello(@RequestParam("name") String name) {
log.info("这是一条日志,参数name={}", name);
return WeaResult.success("hello world");
}
}
**日志获取:** 在运维平台中获取日志
数据库操作
参见:e10平台定制开发指引
E10 执行数据库 SQL 要比 E9 更加复杂
需要先创建 `ExecuteSqlEntity` 对象,用来存放 SQL 语句以及其它的一些信息
- Sql :必需使用 Base64 编码
- GroupId :数据库表对应的服务,可以通过这个接口查到每个模块的 GroupId :/api/datasource/ds/group?sourceType=LOGIC,目前没有方法知道哪个表对应哪个服务,所以只能凭感觉查找,比如说这是一个流程相关的表,那么你就选流程的模块
- SourceType :固定为 SourceType.LOGIC
此方法不支持使用占位符注入参数,只能使用字符串拼接参数
ExecuteSqlEntity sqlEntity = new ExecuteSqlEntity();
String sql = "select person_name,age from uf_personrecord";
sqlEntity.setSql(Base64.encode(sql));
sqlEntity.setGroupId("weaver-ebuilder-form-service");
sqlEntity.setSourceType(SourceType.LOGIC);
之后使用 `dataSetService` 执行 ,获取返回结果,返回的 `HashMap` 键为字段名,值为字段的值
Map<String, Object> result = dataSetService.executeSql(sqlEntity);
// records 可能为null,注意判断
List<HashMap<String ,Object>> records = (List<HashMap<String, Object>>) result.get("records");
for (HashMap<String, Object> row : records) {
row.forEach((k,v) -> log.info("column:"+k+",value:"+v));
}
更多详细使用请查看官方说明文档
使用工具类执行 SQL
为了简化 SQL 的执行,我创建了一个工具类,让它和 E9 的 `RecordSet` 一样简单。
- 操作简单
- 支持使用占位符注入参数
你需要将此公共类库放入你的项目,开源地址:weaver-e10-second-dev-common: 泛微 E10 二次开发公共类库,提供工具类和通用组件,可在任何 E10 二开项目中使用。
使用示例:
String sql = "SELECT " + fieldName + " FROM " + tableName + " WHERE id = ?";
SqlExecuteResult result = sqlExecuteClient.executeSql(groupType, sql, dataId);
if (!result.isSuccess()) {
log.error("查询字段值失败,sql:{},错误信息:{}", sql, result.getMessage());
throw new SqlExecuteException("查询字段值失败,sql:" + sql + ",错误信息:" + result.getMessage());
}
// 获取查询数据
List<Map<String, Object>> records = result.getRecords();
`executeSql()` :
SqlExecuteResult executeSql(DatasourceGroupType groupType, String sql, SqlParam... params)
- groupType:数据库表所属模块
- sql:需要执行的sql,不区分查询和更新
- params:传入 sql 中的参数,如果没有则不需要传
可通过返回结果的 `SqlExecuteResult.isSuccess` 判断是否执行成功
⚠️ 注意:返回结果的字段名可能会为大写,获取时需要注意,你可以使用 `SqlExecuteClient.getFieldValueIgnoreCase()` 方法忽略字段名大小写来获取字段值
缓存
通过 `BaseCache` 来操作缓存,参考:e10平台定制开发指引
示例:
@RestController
@RequestMapping({"/api/secondev/workflow/demo","/papi/secondev/workflow/demo"})
@Slf4j
public class ApiDemoController {
// 默认读weaver-secondev-service\webapps\ROOT\WEB-INF\classes\weaver\config\config-center\weaver-secondev-service.properties文件
@Value("${spring.datasource.username}")
private String name;
@Autowired
private BaseCache baseCache;
@GetMapping("/hello")
public String hello() {
log.info("hello,=> {}",name);
// 写入缓存并设置时间
baseCache.set("dev", "age", 18,60*60);
log.info("age = {}", baseCache.get("dev", "age"));
baseCache.del("dev", "age");
log.info("age = {}", baseCache.get("dev", "age"));
return "hello world";
}
}
消息队列
官方文档说明:e10平台定制开发指引
定时任务
官方文档说明:E10自定义定时任务开发文档
定时任务有两个方法: 1. 通过动作流实现 2. 通过 @ESchedulerHandler 注解实现
@ESchedulerHandler 注解实现示例:
package com.weaver.seconddev.demo.escheduler;
import com.weaver.common.escheduler.context.ESchedulerJobHelper;
import com.weaver.common.escheduler.handler.annotation.ESchedulerHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class DemoEscheduler {
private static final Logger log = LoggerFactory.getLogger(DemoEscheduler.class);
/**
* value: 业务方法的执行名称
* cron: cron表达式自定义执行时间
* @throws Exception
*/
@ESchedulerHandler(value = "demoJobHandler", cron = "0/10 * * * * ?")
public void demoJobHandler() throws Exception {
log.info("demoJobHandler runnint");
try {
// todo
// 获取页面上配置的任务参数
final String jobParam = ESchedulerJobHelper.getJobParam();
} catch (Exception e) {
e.printStackTrace();
log.error("demoJobHandler runnint error");
}
ESchedulerJobHelper.log("ESchedulerHandler, Hello World.");
}
}
你可以通过 ` /info/dispatch/escheduler/report` 页面来查看这些计划任务

动作流介绍
动作流是 E10 的一个强大的功能,它内置了很多功能,比如表单数据修改、接口连接器调用、消息提醒等,可以通过多种形式触发动作流,可在动作流内使用条件进行分叉,控制动作走向。

可进入系统后台,在这个页面管理所有动作流,地址:/info/esb/application

动作流功能包括:
- 内置多种标准组件,比如消息提醒,表单数据修改,数据计算(变量修改)等
- 可添加条件,实现组件根据条件进行流转
- 每个组件都可以获取前面组件的数据
- 可对参数进行动态赋值,例如使用函数计算(可执行js),获取表单字段值(流程中使用)

函数
在动作流中可以创建自定义函数,来进行参数赋值

函数可以在动态赋值对话框中创建,也可以在 ESB 中心的函数列表中创建:`/info/esb/setting/function`

**创建函数**
params 参数可以有多个参数,例如:
function test(param1,param2){
}
示例:
function convertGenderToNum(value){
return value === 'male'? 1: 2;
}
注意:创建函数时要先选择结果类型,因为函数创建之后结果类型就不能更改,你只能删了函数重新创建。 js 中不能包含注释

连接器
连接器可作为动作流中的组件,是一个非常常用的功能,可用于进行接口调用,例如流程归档之后调用第三方系统接口反馈状态。
入口:ESB 中心> 连接器 创建连接器时配置接口地址以及请求参数、返回参数即可,连接器发布后可在动作流中调用此连接器,实现接口调用。

自定义 Action 接口
可在动作流中配置自定义 Action 接口,通过 Action 来进行处理
**应用场景:** 在流程节点后操作添加动作流,在动作流内配置 Action,通过 Action 来实现功能,这个有点像 E9 的流程 Action,不过 E10 的 Action 是通用的,不止是在流程上

Action 具有输入和输出参数,在输入参数中可通过动态赋值,来获取表单字段值或者流程系统参数。输出参数可在动作流后面进行获取,比如说输出参数返回的是成功还是失败,在动作流进行流转控制,或者是在动作流获取输出参数中的结果。
输入参数: 
输出参数: 
创建 Action
在类中实现 `EsbServerlessRpcRemoteInterface` 接口 需要使用 `@Service` 注解,并且配置名称,名称和类名一样即可,如果不一样则可能无法识别该 Action。
execute() 方法的参数对应动作流中 Action 的输入参数,方法返回的 `WeaResult` 中的类型对象,对应动作流中 Action 的输出参数
示例:
package com.weaver.seconddev.hnweaver.demo.action;
import com.weaver.common.base.entity.result.WeaResult;
import com.weaver.esb.api.rpc.EsbServerlessRpcRemoteInterface;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* @author 姚礼林
* @desc 测试 Action
* @date 2024/5/6
*/
@Service("TestAction")
public class TestAction implements EsbServerlessRpcRemoteInterface {
@Override
public WeaResult<Map<String, Object>> execute(Map<String, Object> params) {
WeaResult<Map<String, Object>> result;
if (params.get("name") == null || "1".equals(params.get("name"))) {
result = WeaResult.fail("流程出错啦,name 参数不正确");
return result;
}
Map<String, Object> data = new HashMap<>();
result = WeaResult.success(data);
return result;
}
}
**在 xml 配置文件上配置 Action**
上传后端 jar 包后, 需要在 `/ecode/monitor/loom/deploy/jar` 页面编辑配置文件,配置刚才创建的 Action,注意如果 Action 不存在则会导致二开服务无法启动。

需要在 xml 文件内的 `beans` 标签内添加: 以刚才创建的 Action 为示例
<dubbo:service ref="TestAction"
interface="com.weaver.esb.api.rpc.EsbServerlessRpcRemoteInterface"
group="TestAction" />
- ref: Action 中的 @Service 注解中的名称,不可与其它名称重复
- interface: 固定为 `com.weaver.esb.api.rpc.EsbServerlessRpcRemoteInterface`
- group : 与 ref 一致
配置之后重启二开服务生效
**在动作流上配置 Action**
在动作流中添加操作,选择 serverless/Action

分组标识填写刚才在配置文件上配的 `group`

之后填写输入和输出参数
开放平台
开放平台介绍
开放平台入口:后台中心 -> 开放平台 开放平台接口文档:/sp/opendoc

**什么是开放平台** 开放平台内有许多公开的接口,可以供第三方或者OA系统内部进行调用,也就是这些接口就用来给别人调的
开放平台使用
新建应用和完善开发者资料
需要先新建应用和完善开发者资料才可以调用开放平台接口,可以在开放平台文档查看如何操作:`/sp/opendoc`


在后端中调用开放平台接口
**获取 SDK** 从此处下载 SDK,然后在此页面 `/ecode/monitor/loom/thirdJar` 将 SDK 作为第三方依赖上传到二开服务中,如提示文件格式不对,需要打包成 build.zip 文件进行上传


**获取 Token** 需要 token 才能调用接口,也就是先要进行认证
编写一个 Token 获取工具类,用于获取 Token
package com.weaver.seconddev.hnweaver.common.sdk.util;
import com.weaver.openapi.pojo.auth.params.AccessTokenParam;
import com.weaver.openapi.pojo.auth.params.CodeParam;
import com.weaver.openapi.pojo.auth.res.AccessToken;
import com.weaver.openapi.pojo.auth.res.Code;
import com.weaver.openapi.service.AuthService;
import lombok.experimental.UtilityClass;
/**
* @author 姚礼林
* @desc 开放接口 token 获取工具类
* @date 2025/7/5
**/
@UtilityClass
public class OpenApiTokenUtil {
/**
* 获取开放平台接口 Token
*
* @param appKey 应用 Key
* @param appSecret 应用 Secret
* @param cropid 企业 cropid,可在开发者资料中查看 corpId
* @param state 重定向后会带上state参数,企业可以填写a-zA-Z0-9的参数值,长度不可超过128个字节
* @param serverAddress OA 地址
* @return 开放平台接口 Token
*/
public static String getToken(String appKey, String appSecret, String cropid, String state, String serverAddress) {
AccessTokenParam tokenParam = new AccessTokenParam(appKey, appSecret,
"authorization_code", fetchCode(cropid, state, serverAddress));
AccessToken token = AuthService.getAuthAccessToken(tokenParam,
serverAddress + "/papi/openapi", null);
return token.getAccessToken();
}
private static String fetchCode(String cropid, String state, String serverAddress) {
CodeParam codeParam = new CodeParam(cropid, "code", state);
Code code = AuthService.getAuthCode(codeParam, serverAddress + "/papi/openapi", null);
return code.getCode();
}
}
getToken() 参数说明:
- appKey :可在开放平台的应用中查看
- appSecret:可在开放平台的应用中查看
- cropid:可在开放平台的开发者资料中查看 corpId
- state:随便填
- serverAddress:E10 地址,如 http://127.0.0.1:20600
使用示例:
package com.weaver.seconddev.hnweaver.common.request;
import com.weaver.seconddev.hnweaver.common.sdk.util.OpenApiTokenUtil;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* @author 姚礼林
* @desc todo
* @date 2025/9/26
**/
class OpenApiTokenUtilTest {
@Test
void getToken() {
String token = OpenApiTokenUtil.getToken("301dcdc2a74ba068d687e9359325af44",
"d1abb71bs77ec2h0d4ddf4461b11e4f", "7701caaehg185d19c3je87ad0c4642fa",
"dev", "http://127.0.0.1:20600");
System.out.println(token);
assertFalse(token.isEmpty());
}
}
为了在后端中方便调用,可以创建一个管理 Token 获取的类,将 appKey、appSecret 这些信息配置在配置文件里
示例: 可将 Token 存放到缓存
package com.weaver.seconddev.hnweaver.common.request;
import com.weaver.common.cache.base.BaseCache;
import com.weaver.openapi.pojo.auth.params.AccessTokenParam;
import com.weaver.openapi.pojo.auth.params.CodeParam;
import com.weaver.openapi.pojo.auth.res.AccessToken;
import com.weaver.openapi.pojo.auth.res.Code;
import com.weaver.openapi.service.AuthService;
import com.weaver.seconddev.hnweaver.common.config.OpenApiProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @author 姚礼林
* @desc 获取开放接口 token
* @date 2024/5/30
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class OpenApiTokenManger {
private static final String STATE = "dev";
private static final String MODULE_KEY = "seconddev";
private static final String TOKEN_CACHE_KEY = "openApiAccessToken";
private final BaseCache baseCache;
private final OpenApiProperties properties;
public String fetchAccessToken() {
Object tokenCache = baseCache.get(MODULE_KEY, TOKEN_CACHE_KEY);
if (tokenCache != null) {
return (String) tokenCache;
}
AccessTokenParam tokenParam = new AccessTokenParam(properties.getAppKey(), properties.getAppSecret(),
"authorization_code", fetchCode());
AccessToken token = AuthService.getAuthAccessToken(tokenParam,
properties.getServerAddress()+"/papi/openapi", null);
baseCache.set(MODULE_KEY, TOKEN_CACHE_KEY,token.getAccessToken(),token.getExpiresIn());
return token.getAccessToken();
}
public String getApiUrlPrefix() {
return properties.getServerAddress() + "/papi/openapi";
}
private String fetchCode() {
CodeParam codeParam = new CodeParam(properties.getCropid(), "code", STATE);
Code code = AuthService.getAuthCode(codeParam, properties.getServerAddress()+"/papi/openapi", null);
return code.getCode();
}
}
配置类,存放用于调用开放平台的一些信息。 需要在配置类上配置属性,都以 openapi. 开头,例如:openapi.serverAddress 、openapi.cropid
package com.weaver.seconddev.hnweaver.common.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author 姚礼林
* @desc 开放接口配置属性
* @date 2024/5/31
*/
@Configuration
@ConfigurationProperties(prefix = "openapi")
@Data
public class OpenApiProperties {
private String serverAddress;
private String cropid;
private String appKey;
private String appSecret;
}
**调用 Open API 接口**
该示例使用 SDK 调用部门列表接口
/**
* @author 姚礼林
* @desc TODO
* @date 2024/5/30
*/
@RestController
@WeaPermission(publicPermission = true)
@RequestMapping("/api/secondev/hnweaver/demo/openapi")
public class OpenApiTestController {
@Autowired
private OpenApiTokenManger tokenManger;
@Autowired
private OpenApiProperties properties;
@GetMapping("/departments")
public WeaResult<DeptList> queryDepartmentList() {
DeptListParam deptListParam = new DeptListParam(tokenManger.fetchAccessToken(),null,null,null);
DeptList deptList = DeptService.listDeptV2(deptListParam, properties.getServerAddress()+"/papi/openapi", null);
return WeaResult.success(deptList);
}
}
如果想用 HTTP 请求接口进行调用,在接口参数中添加 `access_token` 参数,传入获取的 token 即可

标准内部接口调用
E10 后端有一些公开的内部接口可以调用,比如获取人员信息,下载文件等
获取用户信息 HrmCommonEmployeeService
该类在微服务下可正常调用 使用 `HrmCommonEmployeeService` 类可以获取到用户信息
⚠️ 注意用户表的id和user_id字段不是同一个值,前端调用系统标准接口参数中的用户id为用户表的id字段
获取用户信息:
// 获取用户对象
SimpleEmployee employee = employeeService.getById(Long.parseLong(userId));
根据用户id和租户key获取用户信息:
User currentUser = UserContext.getCurrentUser();
List<SimpleEmployee> employee = employeeService
.getEmployeeByUserIds(CollUtil.toList(currentUser.getUserId()), currentUser.getTenantKey());
参考文档:
获取当前用户
<span style="color:red">注意:有 getUserId() 和 getEmployeeId(),两个是不一样的,getEmployeeId() 获取的是数据库表的数据id,getUserId() 获取的是数据库表的 user_id 字段,一般用 getEmployeeId()</span>
<span style="color:red">如果是系统远程调用接口可能获取不到当前用户</span>
UserContext.getCurrentUser();
文件下载
可根据文件id获取实体文件(下载),例如下载文档中的文件
注入 FileDownloadService
@Autowired
FileDownloadService downloadService;
获取文件流
FileData fileData = downloadService.downloadFile(1213L);
fileData.getInputStream();