From f64afeb84f0607b6b0c46a3f2e83b613bee16285 Mon Sep 17 00:00:00 2001 From: wang Date: Wed, 10 Jun 2026 17:51:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=A4=A7=E5=8E=85=E5=92=8C?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E6=A0=B7=E5=BC=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jwpy/controller/WtczController.java | 15 +++ .../jwpy/domain/entity/WtczEntity.java | 9 ++ .../jwpy/domain/form/WtczQueryForm.java | 6 + .../business/jwpy/service/WtczService.java | 61 ++++++++++ .../src/api/business/jwpy/wtcz-api.js | 14 +++ .../components/side-menu/recursion-menu.vue | 14 ++- .../top-side-menu/recursion-menu.vue | 14 ++- .../src/store/modules/system/user.js | 60 +++------- .../src/views/jwpy/jwpy/flow/index.vue | 104 ++++++++++++++---- 9 files changed, 222 insertions(+), 75 deletions(-) diff --git a/smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/controller/WtczController.java b/smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/controller/WtczController.java index b1de755..0899ae0 100644 --- a/smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/controller/WtczController.java +++ b/smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/controller/WtczController.java @@ -10,6 +10,7 @@ import net.lab1024.sa.admin.module.business.jwpy.domain.form.WtczAddForm; import net.lab1024.sa.admin.module.business.jwpy.domain.form.WtczUpdateForm; import net.lab1024.sa.admin.module.business.jwpy.domain.vo.WtczVO; import net.lab1024.sa.admin.module.business.jwpy.service.WtczService; +import net.lab1024.sa.base.common.util.SmartRequestUtil; import net.lab1024.sa.base.common.domain.PageResult; import net.lab1024.sa.base.common.domain.ResponseDTO; import net.lab1024.sa.base.common.domain.ValidateList; @@ -69,5 +70,19 @@ public class WtczController { public ResponseDTO getDetail(@PathVariable Long id) { return wtczService.getDetail(id); } + + @Operation(summary = "查询大厅待领工单总数") + @GetMapping("/jwpy/wtcz/unclaimedCount") + public ResponseDTO getUnclaimedCount() { + return ResponseDTO.ok(wtczService.getUnclaimedCount()); + } + + @Operation(summary = "随机认领一个待办工单") + @PostMapping("/jwpy/wtcz/claimRandom") + @SaCheckPermission("task:claim") + public ResponseDTO claimRandom() { + Long employeeId = SmartRequestUtil.getRequestUserId(); + return wtczService.claimRandom(employeeId); + } } diff --git a/smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/entity/WtczEntity.java b/smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/entity/WtczEntity.java index 92ac60f..466be7d 100644 --- a/smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/entity/WtczEntity.java +++ b/smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/entity/WtczEntity.java @@ -195,5 +195,14 @@ public class WtczEntity { @Schema(description = "群众满意情况") private String qzmyqk; + + @Schema(description = "分派认领状态: 0未认领/未分派, 1已认领/已分派") + private Integer assignStatus; + + @Schema(description = "处理人/认领人员工ID (关联 t_employee.employee_id)") + private Long processorId; + + @Schema(description = "工单认领时间") + private LocalDateTime claimTime; } diff --git a/smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/WtczQueryForm.java b/smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/WtczQueryForm.java index 6933276..f7a5b48 100644 --- a/smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/WtczQueryForm.java +++ b/smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/domain/form/WtczQueryForm.java @@ -34,4 +34,10 @@ public class WtczQueryForm extends PageParam { @Schema(description = "删除标记:0未删,1已删") private Boolean deletedFlag; + + @Schema(description = "处理人/认领人员工ID") + private Long processorId; + + @Schema(description = "分派认领状态: 0未认领, 1已认领") + private Integer assignStatus; } diff --git a/smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/service/WtczService.java b/smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/service/WtczService.java index 0a9dd2f..6bec5bc 100644 --- a/smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/service/WtczService.java +++ b/smart-flow-api/sa-admin/src/main/java/net/lab1024/sa/admin/module/business/jwpy/service/WtczService.java @@ -89,6 +89,16 @@ public class WtczService { wrapper.like(WtczEntity::getSjfjxm, queryForm.getSjfjxm()); } + // 处理人/认领人员工ID过滤 + if (queryForm.getProcessorId() != null) { + wrapper.eq(WtczEntity::getProcessorId, queryForm.getProcessorId()); + } + + // 分派认领状态过滤 + if (queryForm.getAssignStatus() != null) { + wrapper.eq(WtczEntity::getAssignStatus, queryForm.getAssignStatus()); + } + // 按 ID 降序排列展示 wrapper.orderByDesc(WtczEntity::getId); @@ -319,5 +329,56 @@ public class WtczService { return ResponseDTO.ok(vo); } + + /** + * 获取当前任务大厅中待认领的工单总数 + * + * @return 待领总数 + */ + public Long getUnclaimedCount() { + return wtczDao.selectCount(Wrappers.lambdaQuery() + .eq(WtczEntity::getAssignStatus, 0) + .isNull(WtczEntity::getProcessorId) + ); + } + + /** + * 随机认领一个未分配工单(带乐观锁重试防并发冲突) + * + * @param employeeId 认领人员工ID + * @return 认领成功后的工单详情 + */ + @Transactional(rollbackFor = Exception.class) + public ResponseDTO claimRandom(Long employeeId) { + int retryTimes = 3; + for (int i = 0; i < retryTimes; i++) { + // 1. 随机筛选一条待领的工单 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(WtczEntity::getAssignStatus, 0) + .isNull(WtczEntity::getProcessorId) + .last("ORDER BY random() LIMIT 1"); + + WtczEntity randomEntity = wtczDao.selectOne(wrapper); + if (randomEntity == null) { + return ResponseDTO.userErrorParam("待分配的任务池已空,无工单可认领"); + } + + // 2. 尝试使用乐观锁加锁占位 + int updated = wtczDao.update(null, Wrappers.lambdaUpdate() + .set(WtczEntity::getProcessorId, employeeId) + .set(WtczEntity::getAssignStatus, 1) + .set(WtczEntity::getClaimTime, java.time.LocalDateTime.now()) + .eq(WtczEntity::getId, randomEntity.getId()) + .isNull(WtczEntity::getProcessorId) + ); + + // 3. 认领成功,查询其完整的 VO 关联数据并返回 + if (updated == 1) { + return getDetail(randomEntity.getId()); + } + log.warn("工单ID {} 随机认领时发生并发冲突,开始第 {} 次重试...", randomEntity.getId(), i + 1); + } + return ResponseDTO.userErrorParam("系统当前繁忙或工单已抢空,请稍后刷新重试"); + } } diff --git a/smart-flow-web/src/api/business/jwpy/wtcz-api.js b/smart-flow-web/src/api/business/jwpy/wtcz-api.js index 06c046a..51c08d5 100644 --- a/smart-flow-web/src/api/business/jwpy/wtcz-api.js +++ b/smart-flow-web/src/api/business/jwpy/wtcz-api.js @@ -48,5 +48,19 @@ export const wtczApi = { */ getDetail: (id) => { return getRequest(`/jwpy/wtcz/detail/${id}`); + }, + + /** + * 随机认领一个待办工单 + */ + claimRandom: () => { + return postRequest('/jwpy/wtcz/claimRandom'); + }, + + /** + * 查询大厅可领取的工单总数 + */ + getUnclaimedCount: () => { + return getRequest('/jwpy/wtcz/unclaimedCount'); } }; diff --git a/smart-flow-web/src/layout/components/side-menu/recursion-menu.vue b/smart-flow-web/src/layout/components/side-menu/recursion-menu.vue index d4fe27d..80e88e4 100644 --- a/smart-flow-web/src/layout/components/side-menu/recursion-menu.vue +++ b/smart-flow-web/src/layout/components/side-menu/recursion-menu.vue @@ -33,7 +33,8 @@ import SubMenu from './sub-menu.vue'; import { router } from '/@/router/index'; import { useAppConfigStore } from '/@/store/modules/system/app-config'; - import { useUserStore, stateIndexMap } from '/@/store/modules/system/user'; + import { useUserStore } from '/@/store/modules/system/user'; + import { useDictStore } from '/@/store/modules/system/dict'; const theme = computed(() => useAppConfigStore().$state.sideMenuTheme); const flatPattern = computed(() => useAppConfigStore().$state.flatPattern); @@ -69,10 +70,15 @@ function updateOpenKeysAndSelectKeys() { let currentMenuId = _.toNumber(currentRoute.name); - // 如果是动态流程子路由,重新映射到高亮子项并关联其父项展开 + // 如果是动态流程子路由,从数据字典动态计算偏移量高亮子菜单项,并关联其父项展开 const tabType = currentRoute.params.tabType; - if (tabType && stateIndexMap[tabType]) { - currentMenuId = currentMenuId * 100 + stateIndexMap[tabType]; + if (tabType) { + const dictStore = useDictStore(); + const statesDict = dictStore.getDictData('WORKFLOW_STATE') || []; + const matchIndex = statesDict.findIndex(item => item.dataValue === tabType); + if (matchIndex !== -1) { + currentMenuId = currentMenuId * 100 + (matchIndex + 1); + } } // 防止在当前页面内操作(或手动合上菜单)时,由于路由参数微调触发 watch 重复弹开已关闭的菜单 diff --git a/smart-flow-web/src/layout/components/top-side-menu/recursion-menu.vue b/smart-flow-web/src/layout/components/top-side-menu/recursion-menu.vue index c4c7c6b..fe4d684 100644 --- a/smart-flow-web/src/layout/components/top-side-menu/recursion-menu.vue +++ b/smart-flow-web/src/layout/components/top-side-menu/recursion-menu.vue @@ -29,7 +29,8 @@ import SubMenu from './sub-menu.vue'; import { router } from '/@/router/index'; import { useAppConfigStore } from '/@/store/modules/system/app-config'; - import { useUserStore, stateIndexMap } from '/@/store/modules/system/user'; + import { useUserStore } from '/@/store/modules/system/user'; + import { useDictStore } from '/@/store/modules/system/dict'; const theme = computed(() => useAppConfigStore().$state.sideMenuTheme); const flatPattern = computed(() => useAppConfigStore().$state.flatPattern); @@ -63,10 +64,15 @@ function updateOpenKeysAndSelectKeys() { let currentMenuId = _.toNumber(currentRoute.name); - // 如果是动态流程子路由,重新映射到高亮子项并关联其父项展开 + // 如果是动态流程子路由,从数据字典动态计算偏移量高亮子菜单项,并关联其父项展开 const tabType = currentRoute.params.tabType; - if (tabType && stateIndexMap[tabType]) { - currentMenuId = currentMenuId * 100 + stateIndexMap[tabType]; + if (tabType) { + const dictStore = useDictStore(); + const statesDict = dictStore.getDictData('WORKFLOW_STATE') || []; + const matchIndex = statesDict.findIndex(item => item.dataValue === tabType); + if (matchIndex !== -1) { + currentMenuId = currentMenuId * 100 + (matchIndex + 1); + } } // 防止在当前页面内操作(或手动合上菜单)时,由于路由参数微调触发 watch 重复弹开已关闭的菜单 diff --git a/smart-flow-web/src/store/modules/system/user.js b/smart-flow-web/src/store/modules/system/user.js index 9d51827..fd577cd 100644 --- a/smart-flow-web/src/store/modules/system/user.js +++ b/smart-flow-web/src/store/modules/system/user.js @@ -15,6 +15,7 @@ import { MENU_TYPE_ENUM } from '/@/constants/system/menu-const'; import { messageApi } from '/@/api/support/message-api.js'; import { smartSentry } from '/@/lib/smart-sentry.js'; import { localRead, localSave, localRemove } from '/@/utils/local-util'; +import { useDictStore } from '/@/store/modules/system/dict'; export const useUserStore = defineStore({ @@ -366,26 +367,6 @@ function buildMenuChildren(menu, allMenuList) { } // ---------------------------- 动态工单流转状态插拔装配 ---------------------------- -export const statePool = { - 'all': { title: '全部工单', queryType: 'all' }, - 'to_handle': { title: '待办工单', queryType: 'to_handle' }, - 'transferred': { title: '已流转工单', queryType: 'transferred' }, - 'about_to_expire': { title: '即将超期工单', queryType: 'about_to_expire' }, - 'expired': { title: '已超期工单', queryType: 'expired' }, - 'completed': { title: '已办结工单', queryType: 'completed' }, - 'invalid': { title: '已作废工单', queryType: 'invalid' } -}; - -export const businessWorkflowConfig = { - default: ['all', 'to_handle', 'transferred', 'about_to_expire', 'expired', 'completed', 'invalid'] -}; - -// 动态根据默认数组顺序生成索引映射,防止后续修改顺序时高亮乱蹦 -export const stateIndexMap = {}; -businessWorkflowConfig.default.forEach((key, index) => { - stateIndexMap[key] = index + 1; -}); - function injectWorkflowSubMenus(menuNode) { if (menuNode.children && menuNode.children.length > 0) { for (const child of menuNode.children) { @@ -395,33 +376,22 @@ function injectWorkflowSubMenus(menuNode) { // 检测组件路径是否为警务评议与问题处置的通用工单列表 if (menuNode.component && menuNode.component.includes('jwpy/flow/index')) { - let states = businessWorkflowConfig.default; - // 支持通过后端菜单的扩展字段配置该业务支持的状态,实现可插拔 - if (menuNode.extendInfo) { - try { - const config = JSON.parse(menuNode.extendInfo); - if (config.workflowStates && config.workflowStates.length > 0) { - states = config.workflowStates; - } - } catch (e) { } - } + const dictStore = useDictStore(); + // 从数据字典加载流转状态项列表,实现真正的动态插拔 + const statesDict = dictStore.getDictData('WORKFLOW_STATE') || []; - const sjlyNo = menuNode.path ? menuNode.path.split('/').pop() : menuNode.menuId.toString(); const children = []; - states.forEach((stateKey, index) => { - const stateObj = statePool[stateKey]; - if (stateObj) { - children.push({ - menuId: menuNode.menuId * 100 + (index + 1), // 生成全局唯一数字 ID - parentId: menuNode.menuId, - menuName: stateObj.title, - path: `${menuNode.path}/${stateObj.queryType}`, - component: menuNode.component, - menuType: MENU_TYPE_ENUM.MENU.value, - visibleFlag: true, - disabledFlag: false - }); - } + statesDict.forEach((item, index) => { + children.push({ + menuId: menuNode.menuId * 100 + (index + 1), // 生成全局唯一数字 ID + parentId: menuNode.menuId, + menuName: item.dataLabel, + path: `${menuNode.path}/${item.dataValue}`, + component: menuNode.component, + menuType: MENU_TYPE_ENUM.MENU.value, + visibleFlag: true, + disabledFlag: false + }); }); menuNode.children = [...(menuNode.children || []), ...children]; diff --git a/smart-flow-web/src/views/jwpy/jwpy/flow/index.vue b/smart-flow-web/src/views/jwpy/jwpy/flow/index.vue index 58aec22..5294c63 100644 --- a/smart-flow-web/src/views/jwpy/jwpy/flow/index.vue +++ b/smart-flow-web/src/views/jwpy/jwpy/flow/index.vue @@ -56,6 +56,12 @@ class="custom-table" >