@ -23,6 +23,7 @@ import apelet.common.flow.util.BaseFlowIdentityExtHelper;
@@ -23,6 +23,7 @@ import apelet.common.flow.util.BaseFlowIdentityExtHelper;
import apelet.common.flow.util.FlowCustomExtFactory ;
import apelet.common.flow.vo.FlowTaskVo ;
import apelet.common.flow.vo.FlowUserInfoVo ;
import apelet.common.flow.vo.TaskInfoVo ;
import cn.hutool.core.collection.CollUtil ;
import cn.hutool.core.convert.Convert ;
import cn.hutool.core.lang.Assert ;
@ -45,6 +46,7 @@ import org.flowable.common.engine.api.delegate.Expression;
@@ -45,6 +46,7 @@ import org.flowable.common.engine.api.delegate.Expression;
import org.flowable.common.engine.impl.de.odysseus.el.ExpressionFactoryImpl ;
import org.flowable.common.engine.impl.de.odysseus.el.util.SimpleContext ;
import org.flowable.common.engine.impl.identity.Authentication ;
import org.flowable.common.engine.api.query.NativeQuery ;
import org.flowable.common.engine.impl.javax.el.ExpressionFactory ;
import org.flowable.common.engine.impl.javax.el.ValueExpression ;
import org.flowable.engine.* ;
@ -123,6 +125,8 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -123,6 +125,8 @@ public class FlowApiServiceImpl implements FlowApiService {
private FlowVariableLogService flowVariableLogService ;
@Autowired
private FlowTaskTimeoutJobService flowTaskTimeoutJobService ;
@Autowired
private FlowTaskInstanceExtService flowTaskInstanceExtService ;
@Transactional ( rollbackFor = Exception . class )
@Override
@ -690,8 +694,8 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -690,8 +694,8 @@ public class FlowApiServiceImpl implements FlowApiService {
}
@Override
public MyPageData < Task > getTaskListByUserName (
String username , String definitionKey , String definitionName , String taskNam e, MyPageParam pageParam ) {
public MyPageData < Task > getTaskListByUserName ( String username , String definitionKey , String definitionName ,
String taskName , String titl e , MyPageParam pageParam ) {
TaskQuery query = this . createQuery ( ) ;
if ( StrUtil . isNotBlank ( definitionKey ) ) {
query . processDefinitionKey ( definitionKey ) ;
@ -705,10 +709,13 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -705,10 +709,13 @@ public class FlowApiServiceImpl implements FlowApiService {
this . buildCandidateCondition ( query , username ) ;
query . orderByTaskCreateTime ( ) . desc ( ) ;
Map < String , Boolean > multiInstanceTaskMap = new HashMap < > ( ) ;
long totalCount = this . countOwnedTaskList ( query , username , multiInstanceTaskMap ) ;
// 标题过滤只能在 Java 侧做:标题存在我们自己的表里,Flowable 的查询构造器表达不了;
// 而本列表的分页本来就是「批扫 + 逐条过滤」的(会签实例对候选者全体可见),跟着同一个后置过滤加最自然。
TitleFilterContext titleFilter = new TitleFilterContext ( title ) ;
long totalCount = this . countOwnedTaskList ( query , username , multiInstanceTaskMap , titleFilter ) ;
int firstResult = ( pageParam . getPageNum ( ) - 1 ) * pageParam . getPageSize ( ) ;
List < Task > taskList = this . listOwnedTaskPage (
query , username , firstResult , pageParam . getPageSize ( ) , multiInstanceTaskMap ) ;
query , username , firstResult , pageParam . getPageSize ( ) , multiInstanceTaskMap , titleFilter ) ;
return new MyPageData < > ( taskList , totalCount ) ;
}
@ -716,7 +723,8 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -716,7 +723,8 @@ public class FlowApiServiceImpl implements FlowApiService {
public long getTaskCountByUserName ( String username ) {
TaskQuery query = this . createQuery ( ) ;
this . buildCandidateCondition ( query , username ) ;
return this . countOwnedTaskList ( query , username , new HashMap < > ( ) ) ;
// 待办数量是角标,不按标题过滤
return this . countOwnedTaskList ( query , username , new HashMap < > ( ) , new TitleFilterContext ( null ) ) ;
}
/ * *
@ -724,7 +732,8 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -724,7 +732,8 @@ public class FlowApiServiceImpl implements FlowApiService {
* 多实例会签节点在流程图中将全部会签人同时配置为候选者 , 导致每个实例对所有人可见 。
* 这里只保留会签实例归属人自己的那一条 , 其他会签人的实例不参与统计 。
* /
private long countOwnedTaskList ( TaskQuery query , String username , Map < String , Boolean > multiInstanceTaskMap ) {
private long countOwnedTaskList ( TaskQuery query , String username , Map < String , Boolean > multiInstanceTaskMap ,
TitleFilterContext titleFilter ) {
long totalCount = 0 ;
int cursor = 0 ;
while ( true ) {
@ -733,8 +742,10 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -733,8 +742,10 @@ public class FlowApiServiceImpl implements FlowApiService {
break ;
}
cursor + = batch . size ( ) ;
this . loadTitles ( batch , titleFilter ) ;
for ( Task task : batch ) {
if ( this . isTaskOwnedByUser ( task , username , multiInstanceTaskMap ) ) {
if ( this . isTaskOwnedByUser ( task , username , multiInstanceTaskMap )
& & this . isTitleMatched ( task , titleFilter ) ) {
totalCount + + ;
}
}
@ -747,7 +758,8 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -747,7 +758,8 @@ public class FlowApiServiceImpl implements FlowApiService {
* 多实例会签节点的实例对候选者全体可见 , 分页时跳过不属于当前用户的其他会签实例 。
* /
private List < Task > listOwnedTaskPage (
TaskQuery query , String username , int firstResult , int pageSize , Map < String , Boolean > multiInstanceTaskMap ) {
TaskQuery query , String username , int firstResult , int pageSize , Map < String , Boolean > multiInstanceTaskMap ,
TitleFilterContext titleFilter ) {
List < Task > resultList = new LinkedList < > ( ) ;
int skipCount = firstResult ;
int cursor = 0 ;
@ -757,8 +769,10 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -757,8 +769,10 @@ public class FlowApiServiceImpl implements FlowApiService {
break ;
}
cursor + = batch . size ( ) ;
this . loadTitles ( batch , titleFilter ) ;
for ( Task task : batch ) {
if ( ! this . isTaskOwnedByUser ( task , username , multiInstanceTaskMap ) ) {
if ( ! this . isTaskOwnedByUser ( task , username , multiInstanceTaskMap )
| | ! this . isTitleMatched ( task , titleFilter ) ) {
continue ;
}
if ( skipCount > 0 ) {
@ -775,6 +789,83 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -775,6 +789,83 @@ public class FlowApiServiceImpl implements FlowApiService {
}
/ * *
* 按批懒加载标题 : 只查本批里还没加载过的实例 , 避免逐条查库 。
*
* @param batch 本批任务 。
* @param titleFilter 标题过滤上下文 , 关键字为空时直接跳过 、 不做任何查询 。
* /
private void loadTitles ( Collection < Task > batch , TitleFilterContext titleFilter ) {
if ( ! titleFilter . enabled ( ) ) {
return ;
}
Set < String > toLoadIdSet = new HashSet < > ( ) ;
for ( Task task : batch ) {
String processInstanceId = task . getProcessInstanceId ( ) ;
if ( StrUtil . isNotBlank ( processInstanceId ) & & titleFilter . loadedInstanceIdSet . add ( processInstanceId ) ) {
toLoadIdSet . add ( processInstanceId ) ;
}
}
if ( CollUtil . isNotEmpty ( toLoadIdSet ) ) {
titleFilter . titleMap . putAll ( flowTaskInstanceExtService . getMapByProcessInstanceIds ( toLoadIdSet ) ) ;
}
}
/ * *
* 判断任务标题是否命中关键字 。
* < p > 标题只认 zz_flow_task_instance_ext 的落库值 : 没有标题行的一律不匹配 , 与列表展示口径一致 。 < / p >
*
* @param task 任务对象 。
* @param titleFilter 标题过滤上下文 , 关键字为空时视为命中 。
* @return 是否命中 。
* /
private boolean isTitleMatched ( Task task , TitleFilterContext titleFilter ) {
if ( ! titleFilter . enabled ( ) ) {
return true ;
}
FlowTaskInstanceExt taskInstanceExt =
titleFilter . titleMap . get ( task . getProcessInstanceId ( ) + ":" + task . getTaskDefinitionKey ( ) ) ;
return taskInstanceExt ! = null & & StrUtil . isNotBlank ( taskInstanceExt . getTitle ( ) )
& & StrUtil . contains ( taskInstanceExt . getTitle ( ) , titleFilter . keyword ) ;
}
/ * *
* 待办列表标题过滤的上下文 。
* < p > 标题存在 zz_flow_task_instance_ext 上 , Flowable 的查询构造器表达不了这个条件 , 只能在 Java 侧过滤 ;
* 而过滤时又要按实例懒加载标题 、 避免逐条查库 。 关键字 、 已加载实例集合 、 标题缓存这三样永远成组出现 ,
* 抽成一个对象 , 免得在几个方法之间来回传六个参数 。 < / p >
* /
private static class TitleFilterContext {
/ * *
* 标题关键字 ; 为空表示不按标题过滤 。
* /
private final String keyword ;
/ * *
* 已经加载过标题的实例Id , 避免同一实例重复查库 。
* /
private final Set < String > loadedInstanceIdSet = new HashSet < > ( ) ;
/ * *
* 标题缓存 , key 为 processInstanceId + ":" + taskKey 。
* /
private final Map < String , FlowTaskInstanceExt > titleMap = new HashMap < > ( ) ;
private TitleFilterContext ( String keyword ) {
this . keyword = keyword ;
}
/ * *
* 是否按标题过滤 。 关键字为空时整个上下文不产生任何查询 。
*
* @return 需要按标题过滤返回 true 。
* /
private boolean enabled ( ) {
return StrUtil . isNotBlank ( keyword ) ;
}
}
/ * *
* 判断任务是否属于当前用户 。
* 多实例会签实例的元素变量 assignee 即该实例真正的归属人 。
* /
@ -906,11 +997,9 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -906,11 +997,9 @@ public class FlowApiServiceImpl implements FlowApiService {
flowWorkOrderService . getInList ( "processInstanceId" , procInstanceIdSet ) ;
Map < String , FlowWorkOrder > workOrderMap =
workOrderList . stream ( ) . collect ( Collectors . toMap ( FlowWorkOrder : : getProcessInstanceId , c - > c ) ) ;
// 标题快照按工单批量预载(一次 IN 查询覆盖整页),避免逐行查扩展对象造成 N+1。
Map < Long , FlowWorkOrderExt > workOrderExtMap = flowWorkOrderService
. getFlowWorkOrderExtByWorkOrderIds ( workOrderList . stream ( )
. map ( FlowWorkOrder : : getWorkOrderId ) . filter ( Objects : : nonNull ) . collect ( Collectors . toSet ( ) ) )
. stream ( ) . collect ( Collectors . toMap ( FlowWorkOrderExt : : getWorkOrderId , c - > c , ( k1 , k2 ) - > k1 ) ) ;
// 节点标题按实例批量预载(一次 IN 查询覆盖整页),避免逐行查询造成 N+1。
Map < String , FlowTaskInstanceExt > taskInstanceExtMap =
flowTaskInstanceExtService . getMapByProcessInstanceIds ( procInstanceIdSet ) ;
List < FlowTaskExt > allFlowTaskExtList = flowTaskExtService . getByProcessDefinitionId ( processDefinitionIdSet ) ;
Map < String , FlowTaskExt > flowTaskExtMap = allFlowTaskExtList . stream ( ) . collect ( Collectors . toMap ( t - > t . getProcessDefinitionId ( ) + ":" + t . getTaskId ( ) , t - > t , ( k1 , k2 ) - > k1 ) ) ;
Map < String , Boolean > multiInstanceTaskMap = new HashMap < > ( ) ;
@ -943,9 +1032,8 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -943,9 +1032,8 @@ public class FlowApiServiceImpl implements FlowApiService {
FlowTaskExt flowTaskExt = flowTaskExtMap . get ( task . getProcessDefinitionId ( ) + ":" + task . getTaskDefinitionKey ( ) ) ;
FlowEntry flowEntry = flowEntryMap . get ( flowEntryPublishMap . get ( task . getProcessDefinitionId ( ) ) . getEntryId ( ) ) ;
if ( flowTaskExt ! = null & & flowEntry ! = null ) {
flowTaskVo . setTitle ( this . resolveTitleCached ( flowTaskExt . getTitleTemplate ( ) ,
task . getProcessInstanceId ( ) , flowWorkOrder , flowEntry , task . getTaskDefinitionKey ( ) ,
flowWorkOrder = = null ? null : workOrderExtMap . get ( flowWorkOrder . getWorkOrderId ( ) ) ) ) ;
flowTaskVo . setTitle ( this . resolveTitleCached ( flowWorkOrder , task . getTaskDefinitionKey ( ) ,
taskInstanceExtMap . get ( task . getProcessInstanceId ( ) + ":" + task . getTaskDefinitionKey ( ) ) ) ) ;
}
flowTaskVoList . add ( flowTaskVo ) ;
}
@ -1022,8 +1110,16 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -1022,8 +1110,16 @@ public class FlowApiServiceImpl implements FlowApiService {
String startUser ,
String beginDate ,
String endDate ,
String title ,
MyPageParam pageParam ,
boolean finishedOnly ) throws ParseException {
if ( StrUtil . isNotBlank ( title ) ) {
// 标题存在 zz_flow_task_instance_ext 上,Flowable 的查询构造器表达不了这个条件;
// 而本接口的分页与总数全由 SQL 决定,Java 侧再过滤会把分页搞坏,所以改走原生 join。
return this . getHistoricProcessInstanceListByTitle (
processDefinitionKey , processDefinitionName , startUser , beginDate , endDate , title ,
pageParam , finishedOnly ) ;
}
HistoricProcessInstanceQuery query = historyService . createHistoricProcessInstanceQuery ( ) ;
if ( StrUtil . isNotBlank ( processDefinitionKey ) ) {
query . processDefinitionKey ( processDefinitionKey ) ;
@ -1062,13 +1158,87 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -1062,13 +1158,87 @@ public class FlowApiServiceImpl implements FlowApiService {
return new MyPageData < > ( instanceList , totalCount ) ;
}
/ * *
* 按首节点标题关键字查询历史流程实例 , 走 Flowable 的原生 join 查询 。
* < p > 过滤条件逐个复刻自 { @code HistoricProcessInstanceQuery } ( 源码 HistoricProcessInstance . xml ) ,
* 漏一个就是 bug , 且只在带关键字时复现 :
* processDefinitionKey → DEF . KEY_ ; processDefinitionName → DEF . NAME_ ; startedBy → RES . START_USER_ID_ ;
* startedAfter / startedBefore → RES . START_TIME_ & gt ; = / & lt ; = ( 注意是闭区间 ) ;
* finished → RES . END_TIME_ is not null ; tenantId / withoutTenantId → RES . TENANT_ID_ 等值 、 或空串 / NULL ;
* orderByProcessInstanceStartTime ( ) . desc ( ) → order by RES . START_TIME_ desc 。 < / p >
* < p > 标题口径与列表展示保持一致 — — 只取 < b > 首节点 < / b > 的标题 。 首节点 taskKey 存在发布版本的
* { @code zz_flow_entry_publish . init_task_info } 里 , 随流程定义而变 , 所以要在 SQL 里用 JSON 函数解析 。
* init_task_info 是 TEXT , 可能不是合法 JSON , 先用 CASE + JSON_VALID 挡一道 , 避免脏数据让整个查询报错 。 < / p >
* < p > 表名 ACT_HI_PROCINST / ACT_RE_PROCDEF 必须写死 — — Flowable 原生查询不支持 $ { prefix } 占位符 。
* 本工程未配置 flowable 的 database - table - prefix , 将来若配置 , 此处表名需同步修改 ;
* Flowable 升级时也需复查列名与语义 。 < / p >
* /
private MyPageData < HistoricProcessInstance > getHistoricProcessInstanceListByTitle (
String processDefinitionKey , String processDefinitionName , String startUser ,
String beginDate , String endDate , String title , MyPageParam pageParam ,
boolean finishedOnly ) throws ParseException {
Map < String , Object > params = new HashMap < > ( 8 ) ;
params . put ( "title" , "%" + this . escapeLikeKeyword ( title ) + "%" ) ;
StringBuilder where = new StringBuilder ( 256 ) ;
where . append ( " WHERE TT.TITLE LIKE #{title}" ) ;
if ( StrUtil . isNotBlank ( processDefinitionKey ) ) {
where . append ( " AND DEF.KEY_ = #{processDefinitionKey}" ) ;
params . put ( "processDefinitionKey" , processDefinitionKey ) ;
}
if ( StrUtil . isNotBlank ( processDefinitionName ) ) {
where . append ( " AND DEF.NAME_ = #{processDefinitionName}" ) ;
params . put ( "processDefinitionName" , processDefinitionName ) ;
}
if ( StrUtil . isNotBlank ( startUser ) ) {
where . append ( " AND RES.START_USER_ID_ = #{startUser}" ) ;
params . put ( "startUser" , startUser ) ;
}
if ( StrUtil . isNotBlank ( beginDate ) ) {
SimpleDateFormat sdf = new SimpleDateFormat ( MyDateUtil . COMMON_SHORT_DATETIME_FORMAT ) ;
where . append ( " AND RES.START_TIME_ >= #{beginDate}" ) ;
params . put ( "beginDate" , sdf . parse ( beginDate ) ) ;
}
if ( StrUtil . isNotBlank ( endDate ) ) {
SimpleDateFormat sdf = new SimpleDateFormat ( MyDateUtil . COMMON_SHORT_DATETIME_FORMAT ) ;
where . append ( " AND RES.START_TIME_ <= #{endDate}" ) ;
params . put ( "endDate" , sdf . parse ( endDate ) ) ;
}
this . appendTenantCondition ( where , params ) ;
if ( finishedOnly ) {
where . append ( " AND RES.END_TIME_ is not null" ) ;
}
// RES.* 之外必须带上 DEF 的四个别名列:原生查询复用的是 historicProcessInstanceResultMap,
// 列表回去要用 getProcessDefinitionKey()/getName(),漏了这几列这两项就是空的。
String fromJoin = " FROM ACT_HI_PROCINST RES"
+ " LEFT OUTER JOIN ACT_RE_PROCDEF DEF ON RES.PROC_DEF_ID_ = DEF.ID_"
+ " INNER JOIN zz_flow_entry_publish FP ON FP.process_definition_id = RES.PROC_DEF_ID_"
+ " INNER JOIN zz_flow_task_instance_ext TT ON TT.PROCESS_INSTANCE_ID = RES.ID_"
+ " AND TT.TASK_KEY = CASE WHEN JSON_VALID(FP.init_task_info)"
+ " THEN JSON_UNQUOTE(JSON_EXTRACT(FP.init_task_info, '$.taskKey')) ELSE NULL END" ;
return this . executeNativeTitleQuery (
historyService . createNativeHistoricProcessInstanceQuery ( ) . sql (
"SELECT RES.*, DEF.KEY_ AS PROC_DEF_KEY_, DEF.NAME_ AS PROC_DEF_NAME_,"
+ " DEF.VERSION_ AS PROC_DEF_VERSION_, DEF.DEPLOYMENT_ID_ AS DEPLOYMENT_ID_"
+ fromJoin + where + " ORDER BY RES.START_TIME_ DESC" ) ,
historyService . createNativeHistoricProcessInstanceQuery ( ) . sql ( "SELECT COUNT(*)" + fromJoin + where ) ,
params , pageParam ) ;
}
@Override
public MyPageData < HistoricTaskInstance > getHistoricTaskInstanceFinishedList (
String processDefinitionName ,
String beginDate ,
String endDate ,
String title ,
MyPageParam pageParam ) throws ParseException {
String loginName = TokenData . takeFromRequest ( ) . getLoginName ( ) ;
if ( StrUtil . isNotBlank ( title ) ) {
// 标题存在 zz_flow_task_instance_ext 上,Flowable 的查询构造器表达不了这个条件;
// 而本接口的分页/排序/总数由 count()+listPage() 决定,Java 侧再过滤会让 totalCount 与页内容错乱,
// 所以带关键字时改走原生 join 查询,把过滤/排序/分页/count 全部下推到 SQL。
return this . getHistoricTaskInstanceFinishedListByTitle (
processDefinitionName , beginDate , endDate , title , loginName , pageParam ) ;
}
HistoricTaskInstanceQuery query = historyService . createHistoricTaskInstanceQuery ( )
. taskAssignee ( loginName )
. finished ( ) ;
@ -1100,6 +1270,109 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -1100,6 +1270,109 @@ public class FlowApiServiceImpl implements FlowApiService {
return new MyPageData < > ( instanceList , totalCount ) ;
}
/ * *
* 按节点标题关键字查询当前用户已办理的任务 , 走 Flowable 的原生 join 查询 。
* < p > 过滤条件逐个复刻自 { @code HistoricTaskInstanceQuery } ( 源码 HistoricTaskInstance . xml ) , 漏一个就是 bug ,
* 且只在带关键字时复现 :
* taskAssignee → ASSIGNEE_ ; finished → END_TIME_ is not null ; taskCompletedAfter / Before → END_TIME_ & gt ; / & lt ; ;
* processDefinitionName → ACT_RE_PROCDEF 的 exists 子查询 ; taskTenantId / taskWithoutTenantId → TENANT_ID_ 等值 、
* 或空串 / NULL ; orderByHistoricTaskInstanceEndTime ( ) . desc ( ) → order by END_TIME_ desc 。 < / p >
* < p > 表名 ACT_HI_TASKINST 必须写死 — — Flowable 的原生查询不支持 $ { prefix } 占位符 ( 实测会被渲染成字面量 ) 。
* 本工程未配置 flowable 的 database - table - prefix , 将来若配置 , 此处表名需同步修改 ;
* Flowable 升级时也需复查上述列名与语义 。 < / p >
* < p > 另 : join 条件命中 zz_flow_task_instance_ext 的唯一键 ( process_instance_id , task_key ) , 走 eq_ref ;
* 但 LIKE ' % kw % ' 用不上索引 , 且选择性差 , MySQL 有可能选错驱动表导致匹配集全量 filesort 。
* 上线前用 EXPLAIN 确认驱动表是 ACT_HI_TASKINST , 必要时把 INNER JOIN 改为 STRAIGHT_JOIN 。 < / p >
* /
private MyPageData < HistoricTaskInstance > getHistoricTaskInstanceFinishedListByTitle (
String processDefinitionName , String beginDate , String endDate , String title ,
String loginName , MyPageParam pageParam ) throws ParseException {
Map < String , Object > params = new HashMap < > ( 8 ) ;
params . put ( "assignee" , loginName ) ;
params . put ( "title" , "%" + this . escapeLikeKeyword ( title ) + "%" ) ;
StringBuilder where = new StringBuilder ( 256 ) ;
where . append ( " WHERE RES.ASSIGNEE_ = #{assignee} AND RES.END_TIME_ is not null AND TT.TITLE LIKE #{title}" ) ;
if ( StrUtil . isNotBlank ( beginDate ) ) {
SimpleDateFormat sdf = new SimpleDateFormat ( MyDateUtil . COMMON_SHORT_DATETIME_FORMAT ) ;
where . append ( " AND RES.END_TIME_ > #{beginDate}" ) ;
params . put ( "beginDate" , sdf . parse ( beginDate ) ) ;
}
if ( StrUtil . isNotBlank ( endDate ) ) {
SimpleDateFormat sdf = new SimpleDateFormat ( MyDateUtil . COMMON_SHORT_DATETIME_FORMAT ) ;
where . append ( " AND RES.END_TIME_ < #{endDate}" ) ;
params . put ( "endDate" , sdf . parse ( endDate ) ) ;
}
if ( StrUtil . isNotBlank ( processDefinitionName ) ) {
where . append ( " AND exists (select 1 from ACT_RE_PROCDEF D" )
. append ( " where RES.PROC_DEF_ID_ = D.ID_ and D.NAME_ = #{processDefinitionName})" ) ;
params . put ( "processDefinitionName" , processDefinitionName ) ;
}
this . appendTenantCondition ( where , params ) ;
String fromJoin = " FROM ACT_HI_TASKINST RES INNER JOIN zz_flow_task_instance_ext TT"
+ " ON TT.PROCESS_INSTANCE_ID = RES.PROC_INST_ID_ AND TT.TASK_KEY = RES.TASK_DEF_KEY_" ;
return this . executeNativeTitleQuery (
historyService . createNativeHistoricTaskInstanceQuery ( )
. sql ( "SELECT RES.*" + fromJoin + where + " ORDER BY RES.END_TIME_ DESC" ) ,
historyService . createNativeHistoricTaskInstanceQuery ( ) . sql ( "SELECT COUNT(*)" + fromJoin + where ) ,
params , pageParam ) ;
}
/ * *
* 转义 LIKE 的通配符 , 避免用户输入里的 % 和 _ 被当成通配符 。
* < p > 反斜杠是 MySQL 的默认转义符 , 自身要先转义 。 < / p >
*
* @param keyword 用户输入的关键字 。
* @return 转义后的关键字 。
* /
private String escapeLikeKeyword ( String keyword ) {
return keyword . replace ( "\\" , "\\\\" ) . replace ( "%" , "\\%" ) . replace ( "_" , "\\_" ) ;
}
/ * *
* 执行 「 list + count 两条同 FROM / WHERE 的原生查询 」 , 两处原生标题查询共用 。
* < p > count 与 list 必须共用同一套 FROM / WHERE 、 且 count 不能带 ORDER BY , 否则 totalCount 与页内容会对不上 。 < / p >
*
* @param listQuery 已设置好 sql 的 list 查询 。
* @param countQuery 已设置好 sql 的 count 查询 。
* @param params 绑参 , 会同时应用到两条查询 。
* @param pageParam 分页对象 。
* @param < R > 查询返回的行类型 。
* @return 分页结果 。
* /
private < R > MyPageData < R > executeNativeTitleQuery (
NativeQuery < ? , R > listQuery , NativeQuery < ? , R > countQuery ,
Map < String , Object > params , MyPageParam pageParam ) {
for ( Map . Entry < String , Object > entry : params . entrySet ( ) ) {
listQuery . parameter ( entry . getKey ( ) , entry . getValue ( ) ) ;
countQuery . parameter ( entry . getKey ( ) , entry . getValue ( ) ) ;
}
long totalCount = countQuery . count ( ) ;
int firstResult = ( pageParam . getPageNum ( ) - 1 ) * pageParam . getPageSize ( ) ;
return new MyPageData < > ( listQuery . listPage ( firstResult , pageParam . getPageSize ( ) ) , totalCount ) ;
}
/ * *
* 追加租户过滤条件 , 两处原生标题查询共用 。
* < p > 语义与 Flowable 的 taskTenantId / taskWithoutTenantId ( 历史流程侧是 processInstanceTenantId /
* processInstanceWithoutTenantId ) 保持一致 : 有租户Id走等值 ; 既没有租户Id也没有 appCode 时按 "无租户" 处理
* ( 空串或 NULL ) ; 否则把 appCode 当租户 。 < / p >
*
* @param where 正在拼装的 WHERE 片段 。
* @param params 绑参容器 , tenantId 参数由本方法写入 。
* /
private void appendTenantCondition ( StringBuilder where , Map < String , Object > params ) {
TokenData tokenData = TokenData . takeFromRequest ( ) ;
if ( tokenData . getTenantId ( ) ! = null ) {
where . append ( " AND RES.TENANT_ID_ = #{tenantId}" ) ;
params . put ( "tenantId" , tokenData . getTenantId ( ) . toString ( ) ) ;
} else if ( StrUtil . isBlank ( tokenData . getAppCode ( ) ) ) {
where . append ( " AND (RES.TENANT_ID_ = '' or RES.TENANT_ID_ is null)" ) ;
} else {
where . append ( " AND RES.TENANT_ID_ = #{tenantId}" ) ;
params . put ( "tenantId" , tokenData . getAppCode ( ) ) ;
}
}
@Override
public List < HistoricActivityInstance > getHistoricActivityInstanceList ( String processInstanceId ) {
return historyService . createHistoricActivityInstanceQuery ( ) . processInstanceId ( processInstanceId ) . list ( ) ;
@ -1183,6 +1456,9 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -1183,6 +1456,9 @@ public class FlowApiServiceImpl implements FlowApiService {
@Override
public void deleteProcessInstance ( String processInstanceId ) {
historyService . deleteHistoricProcessInstance ( processInstanceId ) ;
// deleteHistoricProcessInstance 会级联物理删除该实例的全部 act_hi_taskinst,
// 节点扩展里的标题行不跟着删就成了永远无用的孤儿行。
flowTaskInstanceExtService . deleteByProcessInstanceId ( processInstanceId ) ;
flowMessageService . removeByProcessInstanceId ( processInstanceId ) ;
FlowWorkOrder workOrder = flowWorkOrderService . getFlowWorkOrderByProcessInstanceId ( processInstanceId ) ;
if ( workOrder = = null ) {
@ -2695,22 +2971,14 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -2695,22 +2971,14 @@ public class FlowApiServiceImpl implements FlowApiService {
}
@Override
public String resolveTitleCached ( String titleTemplate , String processInstanceId , FlowWorkOrder workOrder ,
FlowEntry flowEntry , String taskKey , FlowWorkOrderExt workOrderExt ) {
if ( workOrder ! = null & & workOrderExt ! = null & & StrUtil . isNotBlank ( taskKey )
& & StrUtil . isNotBlank ( workOrderExt . getTitleJson ( ) ) ) {
try {
String cached = JSON . parseObject ( workOrderExt . getTitleJson ( ) ) . getString ( taskKey ) ;
if ( StrUtil . isNotBlank ( cached ) ) {
return cached ;
}
} catch ( Exception e ) {
// 快照解析失败不能影响列表整体,降级为实时解析。
log . warn ( "标题快照解析失败,降级为实时解析。workOrderId [{}], taskKey [{}]" ,
workOrder . getWorkOrderId ( ) , taskKey , e ) ;
}
public String resolveTitleCached ( FlowWorkOrder workOrder , String taskKey , FlowTaskInstanceExt taskInstanceExt ) {
// 标题只以 zz_flow_task_instance_ext 的落库值为准,不再回退到按模板实时解析。
// 这样"这个节点到底有没有落库"在列表上一眼可见,排查/联调时不会被实时兜底掩盖。
if ( workOrder = = null | | taskInstanceExt = = null | | StrUtil . isBlank ( taskKey )
| | StrUtil . isBlank ( taskInstanceExt . getTitle ( ) ) ) {
return null ;
}
return this . resolveTitle ( titleTemplate , processInstanceId , workOrder , flowEntry ) ;
return taskInstanceExt . getTitle ( ) ;
}
@Override
@ -2720,13 +2988,13 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -2720,13 +2988,13 @@ public class FlowApiServiceImpl implements FlowApiService {
}
String nodeKey = taskKey ;
if ( StrUtil . isBlank ( nodeKey ) ) {
// 首节点补写场景:create 事件触发时工单还没创建,走到这里工单已建,取当前活动任务节点即可。
Task activeTask = taskService . createTaskQuery ( )
. processInstanceId ( workOrder . getProcessInstanceId ( ) ) . active ( ) . singleResult ( ) ;
if ( activeTask = = null ) {
// 首节点补写场景:create 事件触发时工单还没创建(工单在 flowApiService.start 返回之后才建),
// 走到这里流程可能已经推进过了(autoskip 节点在 start() 内部就被 complete),
// 所以不能取「当前活动任务」,要取该定义的起始节点,与历史流程列表的口径保持一致。
nodeKey = this . getInitTaskKey ( processDefinitionId ) ;
if ( StrUtil . isBlank ( nodeKey ) ) {
return null ;
}
nodeKey = activeTask . getTaskDefinitionKey ( ) ;
}
FlowEntry flowEntry = StrUtil . isBlank ( workOrder . getProcessDefinitionKey ( ) ) ? null
: flowEntryService . getFlowEntryFromCache ( workOrder . getProcessDefinitionKey ( ) ) ;
@ -2740,11 +3008,38 @@ public class FlowApiServiceImpl implements FlowApiService {
@@ -2740,11 +3008,38 @@ public class FlowApiServiceImpl implements FlowApiService {
if ( StrUtil . isBlank ( title ) ) {
return null ;
}
flowWorkOrder Service . saveTaskTitle ( workOrder . getWorkOrder Id ( ) , nodeKey , title ) ;
flowTaskInstanceExt Service . saveTaskTitle ( workOrder . getProcessInstance Id ( ) , processDefinitionId , nodeKey , title ) ;
return title ;
}
/ * *
* 取指定流程定义的起始节点 taskKey 。
* < p > 来源是该发布版本的 initTaskInfo , 与历史流程列表 ( FlowOperationController # listHistoricProcessInstance )
* 取首节点标题模板的口径保持一致 。 < / p >
*
* @param processDefinitionId 流程定义Id 。
* @return 起始节点 taskKey , 取不到返回null 。
* /
private String getInitTaskKey ( String processDefinitionId ) {
if ( StrUtil . isBlank ( processDefinitionId ) ) {
return null ;
}
List < FlowEntryPublish > publishList =
flowEntryService . getFlowEntryPublishList ( CollUtil . newHashSet ( processDefinitionId ) ) ;
for ( FlowEntryPublish publish : publishList ) {
if ( ! StrUtil . equals ( publish . getProcessDefinitionId ( ) , processDefinitionId )
| | StrUtil . isBlank ( publish . getInitTaskInfo ( ) ) ) {
continue ;
}
TaskInfoVo initTaskInfo = JSON . parseObject ( publish . getInitTaskInfo ( ) , TaskInfoVo . class ) ;
if ( initTaskInfo ! = null & & StrUtil . isNotBlank ( initTaskInfo . getTaskKey ( ) ) ) {
return initTaskInfo . getTaskKey ( ) ;
}
}
return null ;
}
/ * *
* 首节点生成标题时还没发生过任何任务完成 , $ { submitUser } 尚未写入 ( 它由 completeTask 设置 ) ,
* 直接解析会留下字面量 。 首节点的 "提交人" 语义上就是发起人 , 故此时把模板里的 $ { submitUser } 换成 $ { initiator } 。
* 第 2 个节点起上一个节点的 complete 已写好 submitUser , 原样返回 。