feat(openapi): 新增对外接口模块,提供门户待办数查询接口 /openapi/countTaskforporter

新增 jeecg-system-openapi 模块,与 local-api/cloud-api 同级,作为对外能力聚合入口。
接口通过 shiro.excludeUrls 白名单放行,鉴权与业务实现完全解耦。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wsm
2026-07-14 16:12:30 +08:00
co-authored by Claude Opus 4.7
parent 83871179ef
commit e7fec1375e
12 changed files with 258 additions and 3 deletions
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>jeecg-system-api</artifactId>
<groupId>org.jeecgframework.boot</groupId>
<version>${jeecgProjectVersion}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jeecg-system-openapi</artifactId>
<!-- 对外接口模块:HTTP 入口 + 能力实现,统一 /openapi/* 前缀。
base-core 由父模块 jeecg-system-api 继承;额外依赖 local-api
便于后续注入 ISysBaseAPI 等接口做跨模块调用。
认证/放行由 base-core 的签名拦截器 + Shiro 按路径统一处理,本模块零鉴权代码。 -->
<dependencies>
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-local-api</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,46 @@
package org.jeecg.openapi.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.openapi.service.ITaskApi;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
/**
* 对外接口 - 任务/待办
* <p>
* 统一前缀 /openapi/*,认证(签名)与放行由框架层按路径统一处理,本类不含任何鉴权代码。
*/
@Tag(name = "对外接口-任务")
@RestController
@RequestMapping("/openapi")
@Slf4j
public class TaskApiController {
@Resource
private ITaskApi taskApi;
/**
* 门户查询待办数
* <p>调用方(门户)通过 username 参数传入 sys_user.username(登录账号)。
*/
@Operation(summary = "门户查询待办数", description = "门户查询待办数")
@GetMapping("/countTaskforporter")
public Result<Long> countTaskforporter(HttpServletRequest request) {
String username = request.getParameter("username");
if (oConvertUtils.isEmpty(username)) {
log.warn("【对外接口-门户待办数】未传入用户账号,无法统计, 参数名: username");
return Result.error("参数 username 不能为空");
}
Long count = taskApi.countTodoByUsername(username);
log.info("【对外接口-门户待办数】统计完成, username: {}, 待办数: {}", username, count);
return Result.OK(count);
}
}
@@ -0,0 +1,30 @@
package org.jeecg.openapi.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* 对外接口 - 任务/待办 通用查询(直查 Flowable 运行时表,不依赖任何业务模块)。
*/
@Mapper
public interface TaskApiMapper {
/**
* 用户待办任务数(直接分配 + 候选人/候选组),仅统计未挂起任务。
* 与 ActivitiServiceImpl#findBaseTodoTasks 的待办口径一致。
*
* @param username 登录账号,对应 ACT_RU_TASK.ASSIGNEE_
*/
@Select("SELECT COUNT(*) FROM ("
+ "(SELECT DISTINCT RES.ID_ FROM ACT_RU_TASK RES "
+ "INNER JOIN ACT_RU_IDENTITYLINK I ON I.TASK_ID_ = RES.ID_ "
+ "WHERE RES.ASSIGNEE_ IS NULL AND I.TYPE_ = 'candidate' "
+ "AND (I.USER_ID_ = #{username} OR I.GROUP_ID_ IN (SELECT g.GROUP_ID_ FROM ACT_ID_MEMBERSHIP g WHERE g.USER_ID_ = #{username})) "
+ "AND RES.SUSPENSION_STATE_ = 1) "
+ "UNION "
+ "(SELECT DISTINCT RES.ID_ FROM ACT_RU_TASK RES "
+ "WHERE RES.ASSIGNEE_ = #{username} AND RES.SUSPENSION_STATE_ = 1)"
+ ") T")
Long countTodo(@Param("username") String username);
}
@@ -0,0 +1,17 @@
package org.jeecg.openapi.service;
/**
* 对外接口 - 任务/待办能力契约
*
* @author openapi
*/
public interface ITaskApi {
/**
* 统计某用户的待办任务数(Flowable:直接分配 + 候选人/候选组)
*
* @param username sys_user.username(登录账号,对应 Flowable ACT_RU_TASK.ASSIGNEE_
* @return 待办数,无数据返回 0
*/
Long countTodoByUsername(String username);
}
@@ -0,0 +1,25 @@
package org.jeecg.openapi.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.openapi.mapper.TaskApiMapper;
import org.jeecg.openapi.service.ITaskApi;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* {@link ITaskApi} 的实现。通用能力,直查 Flowable 表,不依赖业务模块。
*/
@Service
@Slf4j
public class TaskApiImpl implements ITaskApi {
@Resource
private TaskApiMapper taskApiMapper;
@Override
public Long countTodoByUsername(String username) {
Long count = taskApiMapper.countTodo(username);
return count == null ? 0L : count;
}
}
@@ -0,0 +1,65 @@
package org.jeecg.openapi.controller;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import javax.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
import org.jeecg.openapi.service.ITaskApi;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class TaskApiControllerTest {
@Mock
private ITaskApi taskApi;
@Mock
private HttpServletRequest request;
@InjectMocks
private TaskApiController controller;
@Test
void 传入用户账号时应返回待办数() {
when(request.getParameter("username")).thenReturn("admin");
when(taskApi.countTodoByUsername("admin")).thenReturn(3L);
Result<Long> result = controller.countTaskforporter(request);
assertTrue(result.isSuccess());
assertEquals(3L, result.getResult());
verify(taskApi).countTodoByUsername("admin");
}
@Test
void 用户账号为空时应返回参数错误且不查询() {
when(request.getParameter("username")).thenReturn(null);
Result<Long> result = controller.countTaskforporter(request);
assertFalse(result.isSuccess());
assertEquals("参数 username 不能为空", result.getMessage());
verify(taskApi, never()).countTodoByUsername(any());
}
@Test
void 用户账号为空串时应返回参数错误且不查询() {
when(request.getParameter("username")).thenReturn("");
Result<Long> result = controller.countTaskforporter(request);
assertFalse(result.isSuccess());
verify(taskApi, never()).countTodoByUsername(any());
}
}
@@ -0,0 +1,35 @@
package org.jeecg.openapi.service.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
import org.jeecg.openapi.mapper.TaskApiMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class TaskApiImplTest {
@Mock
private TaskApiMapper taskApiMapper;
@InjectMocks
private TaskApiImpl taskApi;
@Test
void 应返回Mapper查询到的待办数() {
when(taskApiMapper.countTodo("admin")).thenReturn(5L);
assertEquals(5L, taskApi.countTodoByUsername("admin"));
}
@Test
void Mapper返回null时应归零() {
when(taskApiMapper.countTodo("nobody")).thenReturn(null);
assertEquals(0L, taskApi.countTodoByUsername("nobody"));
}
}
@@ -15,6 +15,7 @@
<modules> <modules>
<module>jeecg-system-local-api</module> <module>jeecg-system-local-api</module>
<module>jeecg-system-cloud-api</module> <module>jeecg-system-cloud-api</module>
<module>jeecg-system-openapi</module>
</modules> </modules>
<dependencies> <dependencies>
@@ -31,6 +31,11 @@
<groupId>org.jeecgframework.boot</groupId> <groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-module-flow</artifactId> <artifactId>jeecg-module-flow</artifactId>
</dependency> </dependency>
<!-- 对外接口模块 -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-openapi</artifactId>
</dependency>
<!-- flyway 数据库自动升级 --> <!-- flyway 数据库自动升级 -->
<dependency> <dependency>
@@ -251,7 +251,7 @@ jeecg:
#webapp文件路径 #webapp文件路径
webapp: /opt/jeecg-boot/webapp webapp: /opt/jeecg-boot/webapp
shiro: shiro:
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/api/getUserInfo excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/api/getUserInfo,/openapi/*
#阿里云oss存储和大鱼短信秘钥配置 #阿里云oss存储和大鱼短信秘钥配置
oss: oss:
accessKey: ?? accessKey: ??
@@ -250,7 +250,7 @@ jeecg:
#webapp文件路径 #webapp文件路径
webapp: D://opt//webapp webapp: D://opt//webapp
shiro: shiro:
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/** excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/openapi/*
#阿里云oss存储和大鱼短信秘钥配置 #阿里云oss存储和大鱼短信秘钥配置
oss: oss:
accessKey: ?? accessKey: ??
+6
View File
@@ -267,6 +267,12 @@
<artifactId>jeecg-system-cloud-api</artifactId> <artifactId>jeecg-system-cloud-api</artifactId>
<version>${jeecgProjectVersion}</version> <version>${jeecgProjectVersion}</version>
</dependency> </dependency>
<!-- system 对外接口 openapi -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-openapi</artifactId>
<version>${jeecgProjectVersion}</version>
</dependency>
<!-- 流程引擎 api --> <!-- 流程引擎 api -->
<dependency> <dependency>