Compare commits
7 Commits
c9549bed46
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
679edf08ba | ||
| e71140d8a2 | |||
| 5c7dbf7286 | |||
| 9f03dc27cc | |||
| d669d53a80 | |||
| df13a90107 | |||
| 8a36b49128 |
@@ -105,7 +105,11 @@
|
|||||||
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; $env:PATH = \"$env:JAVA_HOME\\\\bin;$env:PATH\"; $out = & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am compile -DskipTests 2>&1; $code = $LASTEXITCODE; $out | Select-Object -Last 15; Write-Output \"EXIT=$code\")",
|
"PowerShell($env:JAVA_HOME = 'C:\\\\Program Files\\\\Java\\\\jdk-17'; $env:PATH = \"$env:JAVA_HOME\\\\bin;$env:PATH\"; $out = & 'C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd' -pl rdms-project/rdms-project-boot -am compile -DskipTests 2>&1; $code = $LASTEXITCODE; $out | Select-Object -Last 15; Write-Output \"EXIT=$code\")",
|
||||||
"Bash(set \"JAVA_HOME=C:\\\\Program Files\\\\Java\\\\jdk-17\")",
|
"Bash(set \"JAVA_HOME=C:\\\\Program Files\\\\Java\\\\jdk-17\")",
|
||||||
"Bash(\"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am compile -DskipTests -q)",
|
"Bash(\"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am compile -DskipTests -q)",
|
||||||
"Bash(\"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am compile -DskipTests)"
|
"Bash(\"C:\\\\software\\\\apache-maven-3.8.9\\\\bin\\\\mvn.cmd\" -pl rdms-project/rdms-project-boot -am compile -DskipTests)",
|
||||||
|
"Bash(grep -E \"\\\\.\\(sql|java|md\\)$\")",
|
||||||
|
"Bash(xargs grep -l \"INSERT INTO.*system_menu\")",
|
||||||
|
"Bash(Get-ChildItem *)",
|
||||||
|
"Bash(Select-Object FullName)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
- 描述仓库现状以**当前**代码、配置、文档可验证的事实为准;不要拿历史实现、过渡方案或已废弃模型解释当前状态。
|
- 描述仓库现状以**当前**代码、配置、文档可验证的事实为准;不要拿历史实现、过渡方案或已废弃模型解释当前状态。
|
||||||
- **输出极简**:先给结论、改动点、必要风险;用自然语言给判断和影响面,少贴代码片段;涉及代码用 `file_path:line_number` 引用;用户追问再展开。
|
- **输出极简**:先给结论、改动点、必要风险;用自然语言给判断和影响面,少贴代码片段;涉及代码用 `file_path:line_number` 引用;用户追问再展开。
|
||||||
- **下定论需要充足证据**。疑似 bug 时先判断是否稳定复现:跑了很久没动过的功能**首次**报错,优先怀疑运行时状态污染(devtools / IDE 热替换、ApplicationContext 残留、缓存、Redis / DB 连接、JVM 静态字段被旧 context 设过等),**不要凭单次堆栈就断言代码 bug,更不要直接甩修改方案**。先给"可能原因 + 最便宜的取证步骤"(多数场景是**冷重启 JVM**),用户确认能稳定复现,再讨论代码层面的修复。同款写法在仓库其它位置存在并不能反推"也是 bug",长期能跑的代码突然失效 ≠ 代码本身错。
|
- **下定论需要充足证据**。疑似 bug 时先判断是否稳定复现:跑了很久没动过的功能**首次**报错,优先怀疑运行时状态污染(devtools / IDE 热替换、ApplicationContext 残留、缓存、Redis / DB 连接、JVM 静态字段被旧 context 设过等),**不要凭单次堆栈就断言代码 bug,更不要直接甩修改方案**。先给"可能原因 + 最便宜的取证步骤"(多数场景是**冷重启 JVM**),用户确认能稳定复现,再讨论代码层面的修复。同款写法在仓库其它位置存在并不能反推"也是 bug",长期能跑的代码突然失效 ≠ 代码本身错。
|
||||||
|
- **技术风险判断(性能 / N+1 / 索引缺失 / 架构缺陷 / 并发安全 / 内存泄漏 等)与"bug 判断"同等严格**:未读到实现层不下结论。不要凭 subagent 摘要、字段名、注释或印象"顺嘴提一句风险/瓶颈/可能问题"——那也是下结论,**且杀伤力更大**:用户会基于"风险提示"决定要不要立项整改。如果当前上下文没核实到实现,就明说"这部分未核实,需要打开 X 文件确认",不要把猜测包装成"风险提示"塞出去。已识别教训:执行进度查询答完"已批量聚合无 N+1"后又凭印象抛"列表 N+1 风险",被追问才收回。
|
||||||
|
|
||||||
## 本机环境
|
## 本机环境
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@
|
|||||||
- **对象内接口绝不能挂 `@PreAuthorize("@ss.hasPermission(...)")`**。该注解走的链路在 `PermissionServiceImpl` 里强制按 GLOBAL 取角色(line 343-347)+ 强制按 GLOBAL 查菜单(line 92-94),对象域角色与对象域菜单都进不来,即使授权配置完全正确也必然 403。
|
- **对象内接口绝不能挂 `@PreAuthorize("@ss.hasPermission(...)")`**。该注解走的链路在 `PermissionServiceImpl` 里强制按 GLOBAL 取角色(line 343-347)+ 强制按 GLOBAL 查菜单(line 92-94),对象域角色与对象域菜单都进不来,即使授权配置完全正确也必然 403。
|
||||||
- **对象域权限校验必须落在 Service 层 `@CheckObjectPermission`**,原因:路径里 `objectId` 通常以 `#projectId`/`#productId` 等 SpEL 解析,Controller 的参数校验前置阶段不便复用;与同模块(`ProjectMemberServiceImpl` / `ProjectExecutionServiceImpl` / `ProjectExecutionAssigneeServiceImpl` / `ProjectTaskServiceImpl`)保持一致。
|
- **对象域权限校验必须落在 Service 层 `@CheckObjectPermission`**,原因:路径里 `objectId` 通常以 `#projectId`/`#productId` 等 SpEL 解析,Controller 的参数校验前置阶段不便复用;与同模块(`ProjectMemberServiceImpl` / `ProjectExecutionServiceImpl` / `ProjectExecutionAssigneeServiceImpl` / `ProjectTaskServiceImpl`)保持一致。
|
||||||
- **同一接口不要两条通道叠加**。要么全域,要么对象域;叠加只会让对象域用户被全域那条卡死。
|
- **同一接口不要两条通道叠加**。要么全域,要么对象域;叠加只会让对象域用户被全域那条卡死。
|
||||||
- 列表/详情这类对象内**读路径**目前未挂 `@CheckObjectPermission`(属已识别负债,台账 TD-001),新增读接口暂沿用现状即可,不要顺手改造,等独立立项。
|
- 对象内**读路径**(列表 / 详情 / 状态看板 / 聚合)已统一在 **Service 层**挂 `@CheckObjectPermission(objectType=PROJECT, permission=...PERMISSION_QUERY)`——查询同样要扫库耗资源,必须按对象域鉴权(原台账 TD-001 所述"读路径未挂"已不成立)。**Controller 方法层一律不挂权限注解**,对象域鉴权全部落 Service;新增读接口照此在 Service 层挂对象域权限,不要只在 Controller 留空、更不要误判"Controller 没注解 = 无鉴权"。
|
||||||
|
|
||||||
判定口诀:**URL 里有 `{projectId}` / `{productId}` 等对象 ID → 对象域;没有 → 全域**。
|
判定口诀:**URL 里有 `{projectId}` / `{productId}` 等对象 ID → 对象域;没有 → 全域**。
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.njcn.rdms.framework.common.util.json.JsonUtils;
|
|||||||
import com.njcn.rdms.gateway.util.SecurityFrameworkUtils;
|
import com.njcn.rdms.gateway.util.SecurityFrameworkUtils;
|
||||||
import com.njcn.rdms.gateway.util.WebFrameworkUtils;
|
import com.njcn.rdms.gateway.util.WebFrameworkUtils;
|
||||||
import com.njcn.rdms.module.system.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.system.enums.ErrorCodeConstants;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
|
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
|
||||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||||
@@ -30,6 +31,7 @@ import java.util.function.Function;
|
|||||||
|
|
||||||
import static com.njcn.rdms.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
|
import static com.njcn.rdms.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
|
public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
|
||||||
|
|
||||||
@@ -57,8 +59,16 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LoginUser load(String token) {
|
public LoginUser load(String token) {
|
||||||
String body = checkAccessToken(token).block();
|
// 仅异步 refresh 走这里(同步链路用 getIfPresent + 直接 checkAccessToken,不触发 load)
|
||||||
return buildUser(body, token);
|
// 远端 token 已过期/校验失败时吞掉 ServiceException:
|
||||||
|
// 若抛出,会被 Guava 包成 ExecutionException 并由刷新线程池作为 UncaughtException 打到日志,看起来像故障。
|
||||||
|
try {
|
||||||
|
String body = checkAccessToken(token).block();
|
||||||
|
return buildUser(body, token);
|
||||||
|
} catch (ServiceException ex) {
|
||||||
|
log.info("[loginUserCache] 异步刷新忽略 token 校验失败:code={}, msg={}", ex.getCode(), ex.getMessage());
|
||||||
|
return LOGIN_USER_EMPTY;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -227,4 +227,17 @@ public interface ErrorCodeConstants {
|
|||||||
ErrorCode PERSONAL_ITEM_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_008_007, "当前个人事项状态不允许编辑");
|
ErrorCode PERSONAL_ITEM_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_008_007, "当前个人事项状态不允许编辑");
|
||||||
ErrorCode PERSONAL_ITEM_NOT_ALLOW_DELETE = new ErrorCode(1_008_008_008, "仅初始态(待开始)的个人事项允许删除");
|
ErrorCode PERSONAL_ITEM_NOT_ALLOW_DELETE = new ErrorCode(1_008_008_008, "仅初始态(待开始)的个人事项允许删除");
|
||||||
ErrorCode PERSONAL_ITEM_WRITE_FORBIDDEN = new ErrorCode(1_008_008_009, "无权修改个人事项");
|
ErrorCode PERSONAL_ITEM_WRITE_FORBIDDEN = new ErrorCode(1_008_008_009, "无权修改个人事项");
|
||||||
|
|
||||||
|
// ========== 加班申请 1_008_009_xxx ==========
|
||||||
|
ErrorCode OVERTIME_APPLICATION_NOT_EXISTS = new ErrorCode(1_008_009_001, "加班申请不存在");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_009_002, "加班申请状态定义不存在或已停用");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_009_003, "当前加班申请状态不支持动作【{}】");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_009_004, "动作【{}】必须填写原因");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_009_005, "加班申请状态已发生变化,请刷新后重试");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_APPLICANT_ONLY = new ErrorCode(1_008_009_006, "仅申请人可执行该操作");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_APPROVER_ONLY = new ErrorCode(1_008_009_007, "仅当前审核人可执行该操作");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_APPROVER_INVALID = new ErrorCode(1_008_009_008, "审核人不是有效系统用户");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_APPROVER_SELF_FORBIDDEN = new ErrorCode(1_008_009_009, "审核人不能选择申请人本人");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_READ_FORBIDDEN = new ErrorCode(1_008_009_010, "无权查看该加班申请");
|
||||||
|
ErrorCode OVERTIME_APPLICATION_DELETE_ONLY_CANCELLED = new ErrorCode(1_008_009_011, "仅已撤销的加班申请允许删除");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.njcn.rdms.module.project.constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加班申请常量。
|
||||||
|
*/
|
||||||
|
public final class OvertimeApplicationConstants {
|
||||||
|
|
||||||
|
private OvertimeApplicationConstants() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String BIZ_TYPE = "overtime_application";
|
||||||
|
public static final String STATUS_OBJECT_TYPE = BIZ_TYPE;
|
||||||
|
|
||||||
|
public static final String STATUS_PENDING = "pending";
|
||||||
|
public static final String STATUS_APPROVED = "approved";
|
||||||
|
public static final String STATUS_REJECTED = "rejected";
|
||||||
|
public static final String STATUS_CANCELLED = "cancelled";
|
||||||
|
|
||||||
|
public static final String ACTION_SUBMIT = "submit";
|
||||||
|
public static final String ACTION_RESUBMIT = "resubmit";
|
||||||
|
public static final String ACTION_APPROVE = "approve";
|
||||||
|
public static final String ACTION_REJECT = "reject";
|
||||||
|
public static final String ACTION_CANCEL = "cancel";
|
||||||
|
public static final String ACTION_DELETE = "delete";
|
||||||
|
|
||||||
|
public static final String PERMISSION_QUERY = "project:overtime-application:query";
|
||||||
|
public static final String PERMISSION_CREATE = "project:overtime-application:create";
|
||||||
|
public static final String PERMISSION_UPDATE = "project:overtime-application:update";
|
||||||
|
public static final String PERMISSION_DELETE = "project:overtime-application:delete";
|
||||||
|
public static final String PERMISSION_APPROVE = "project:overtime-application:approve";
|
||||||
|
public static final String PERMISSION_EXPORT = "project:overtime-application:export";
|
||||||
|
|
||||||
|
}
|
||||||
@@ -20,6 +20,13 @@ public final class ProjectExecutionConstants {
|
|||||||
*/
|
*/
|
||||||
public static final String BIZ_TYPE = "project_execution";
|
public static final String BIZ_TYPE = "project_execution";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行读路径查询权限码(对象域,object_type='project')。
|
||||||
|
* 覆盖执行对象所有读路径:page / status-board / detail。
|
||||||
|
* "我参与 / 所有"视角由前端发不发 involveUserId 决定;进得来 = 看项目下全部,无此权限码直接 403。
|
||||||
|
*/
|
||||||
|
public static final String PERMISSION_QUERY = "project:execution:query";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建执行权限码。
|
* 创建执行权限码。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ public final class ProjectTaskConstants {
|
|||||||
*/
|
*/
|
||||||
public static final String BIZ_TYPE = "project_task";
|
public static final String BIZ_TYPE = "project_task";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务读路径查询权限码(对象域,object_type='project')。
|
||||||
|
* 覆盖任务对象所有读路径:page / status-board / board-page / detail / summary(含跨执行 aggregate)。
|
||||||
|
* "我参与 / 所有"视角由前端发不发 involveUserId 决定;进得来 = 看项目下全部,无此权限码直接 403。
|
||||||
|
*/
|
||||||
|
public static final String PERMISSION_QUERY = "project:task:query";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建任务权限码。
|
* 创建任务权限码。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.overtime;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.apilog.core.annotation.ApiAccessLog;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
|
import com.njcn.rdms.framework.excel.core.util.ExcelUtils;
|
||||||
|
import com.njcn.rdms.module.project.constant.OvertimeApplicationConstants;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationExportVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationSaveReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusActionReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusLogRespVO;
|
||||||
|
import com.njcn.rdms.module.project.service.overtime.OvertimeApplicationService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.njcn.rdms.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
|
||||||
|
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
|
@Tag(name = "管理后台 - 加班申请")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/project/overtime-applications")
|
||||||
|
@Validated
|
||||||
|
public class OvertimeApplicationController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private OvertimeApplicationService overtimeApplicationService;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@Operation(summary = "新增加班申请并提交")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_CREATE + "')")
|
||||||
|
public CommonResult<Long> create(@Valid @RequestBody OvertimeApplicationSaveReqVO reqVO) {
|
||||||
|
return success(overtimeApplicationService.createApplication(reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
@Operation(summary = "退回后修改并重新提交加班申请")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_UPDATE + "')")
|
||||||
|
public CommonResult<Boolean> updateRejected(@PathVariable("id") Long id,
|
||||||
|
@Valid @RequestBody OvertimeApplicationSaveReqVO reqVO) {
|
||||||
|
overtimeApplicationService.updateRejectedApplication(id, reqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
@Operation(summary = "获取加班申请详情")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_QUERY + "')")
|
||||||
|
public CommonResult<OvertimeApplicationRespVO> get(@PathVariable("id") Long id) {
|
||||||
|
return success(overtimeApplicationService.getApplication(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/page")
|
||||||
|
@Operation(summary = "获取我的加班申请分页")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_QUERY + "')")
|
||||||
|
public CommonResult<PageResult<OvertimeApplicationRespVO>> page(@Valid OvertimeApplicationPageReqVO reqVO) {
|
||||||
|
return success(overtimeApplicationService.getMyPage(reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/approval-page")
|
||||||
|
@Operation(summary = "获取待我审批的加班申请分页")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_APPROVE + "')")
|
||||||
|
public CommonResult<PageResult<OvertimeApplicationRespVO>> approvalPage(@Valid OvertimeApplicationPageReqVO reqVO) {
|
||||||
|
return success(overtimeApplicationService.getApprovalPage(reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/approve")
|
||||||
|
@Operation(summary = "审核通过加班申请")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_APPROVE + "')")
|
||||||
|
public CommonResult<Boolean> approve(@PathVariable("id") Long id,
|
||||||
|
@Valid @RequestBody OvertimeApplicationStatusActionReqVO reqVO) {
|
||||||
|
overtimeApplicationService.approve(id, reqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/reject")
|
||||||
|
@Operation(summary = "审核退回加班申请")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_APPROVE + "')")
|
||||||
|
public CommonResult<Boolean> reject(@PathVariable("id") Long id,
|
||||||
|
@Valid @RequestBody OvertimeApplicationStatusActionReqVO reqVO) {
|
||||||
|
overtimeApplicationService.reject(id, reqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/cancel")
|
||||||
|
@Operation(summary = "撤销加班申请")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_UPDATE + "')")
|
||||||
|
public CommonResult<Boolean> cancel(@PathVariable("id") Long id,
|
||||||
|
@Valid @RequestBody OvertimeApplicationStatusActionReqVO reqVO) {
|
||||||
|
overtimeApplicationService.cancel(id, reqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@Operation(summary = "删除已撤销的加班申请")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_DELETE + "')")
|
||||||
|
public CommonResult<Boolean> delete(@PathVariable("id") Long id) {
|
||||||
|
overtimeApplicationService.deleteApplication(id);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/status-logs")
|
||||||
|
@Operation(summary = "获取加班申请状态日志")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_QUERY + "')")
|
||||||
|
public CommonResult<List<OvertimeApplicationStatusLogRespVO>> statusLogs(@PathVariable("id") Long id) {
|
||||||
|
return success(overtimeApplicationService.getStatusLogs(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/export")
|
||||||
|
@Operation(summary = "导出我的加班申请")
|
||||||
|
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_EXPORT + "')")
|
||||||
|
@ApiAccessLog(operateType = EXPORT)
|
||||||
|
public void export(@Valid OvertimeApplicationPageReqVO reqVO, HttpServletResponse response) throws IOException {
|
||||||
|
List<OvertimeApplicationExportVO> list = overtimeApplicationService.getExportList(reqVO);
|
||||||
|
ExcelUtils.write(response, "加班申请_" + LocalDate.now() + ".xls", "加班申请",
|
||||||
|
OvertimeApplicationExportVO.class, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
|
||||||
|
|
||||||
|
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
|
||||||
|
import cn.idev.excel.annotation.ExcelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ExcelIgnoreUnannotated
|
||||||
|
public class OvertimeApplicationExportVO {
|
||||||
|
|
||||||
|
@ExcelProperty("申请人")
|
||||||
|
private String applicantName;
|
||||||
|
|
||||||
|
@ExcelProperty("加班日期")
|
||||||
|
private LocalDate overtimeDate;
|
||||||
|
|
||||||
|
@ExcelProperty("加班时长")
|
||||||
|
private String overtimeDuration;
|
||||||
|
|
||||||
|
@ExcelProperty("加班原因")
|
||||||
|
private String overtimeReason;
|
||||||
|
|
||||||
|
@ExcelProperty("加班内容")
|
||||||
|
private String overtimeContent;
|
||||||
|
|
||||||
|
@ExcelProperty("状态")
|
||||||
|
private String statusName;
|
||||||
|
|
||||||
|
@ExcelProperty("审核人")
|
||||||
|
private String approverName;
|
||||||
|
|
||||||
|
@ExcelProperty("提交时间")
|
||||||
|
private LocalDateTime submitTime;
|
||||||
|
|
||||||
|
@ExcelProperty("审核时间")
|
||||||
|
private LocalDateTime approvalTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
|
||||||
|
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 加班申请分页 Request VO")
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class OvertimeApplicationPageReqVO extends PageParam {
|
||||||
|
|
||||||
|
@Schema(description = "关键词,匹配加班原因或加班内容", example = "上线")
|
||||||
|
private String keyword;
|
||||||
|
|
||||||
|
@Schema(description = "申请人姓名,模糊匹配", example = "张三")
|
||||||
|
private String applicantName;
|
||||||
|
|
||||||
|
@Schema(description = "审核人用户编号", example = "1001")
|
||||||
|
private Long approverId;
|
||||||
|
|
||||||
|
@Schema(description = "审核人姓名,模糊匹配", example = "李四")
|
||||||
|
private String approverName;
|
||||||
|
|
||||||
|
@Schema(description = "状态编码", example = "pending")
|
||||||
|
@Size(max = 32, message = "状态编码长度不能超过32个字符")
|
||||||
|
private String statusCode;
|
||||||
|
|
||||||
|
@Schema(description = "加班日期范围", example = "[2026-06-01, 2026-06-30]")
|
||||||
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
|
||||||
|
private LocalDate[] overtimeDate;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间", example = "[2026-06-01 00:00:00, 2026-06-30 23:59:59]")
|
||||||
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||||
|
private LocalDateTime[] createTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 加班申请 Response VO")
|
||||||
|
@Data
|
||||||
|
public class OvertimeApplicationRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "加班申请编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9001")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "申请人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3001")
|
||||||
|
private Long applicantId;
|
||||||
|
|
||||||
|
@Schema(description = "申请人姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
|
||||||
|
private String applicantName;
|
||||||
|
|
||||||
|
@Schema(description = "加班日期", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDate overtimeDate;
|
||||||
|
|
||||||
|
@Schema(description = "加班时长", requiredMode = Schema.RequiredMode.REQUIRED, example = "1天")
|
||||||
|
private String overtimeDuration;
|
||||||
|
|
||||||
|
@Schema(description = "加班原因", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String overtimeReason;
|
||||||
|
|
||||||
|
@Schema(description = "加班内容", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String overtimeContent;
|
||||||
|
|
||||||
|
@Schema(description = "审核人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
|
||||||
|
private Long approverId;
|
||||||
|
|
||||||
|
@Schema(description = "审核人姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
|
||||||
|
private String approverName;
|
||||||
|
|
||||||
|
@Schema(description = "状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending")
|
||||||
|
private String statusCode;
|
||||||
|
|
||||||
|
@Schema(description = "状态名称", example = "待审批")
|
||||||
|
private String statusName;
|
||||||
|
|
||||||
|
@Schema(description = "当前状态是否允许编辑", example = "false")
|
||||||
|
private Boolean allowEdit;
|
||||||
|
|
||||||
|
@Schema(description = "是否终态", example = "false")
|
||||||
|
private Boolean terminal;
|
||||||
|
|
||||||
|
@Schema(description = "最近一次审核意见")
|
||||||
|
private String approvalComment;
|
||||||
|
|
||||||
|
@Schema(description = "提交时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDateTime submitTime;
|
||||||
|
|
||||||
|
@Schema(description = "最近一次审核时间")
|
||||||
|
private LocalDateTime approvalTime;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 加班申请保存 Request VO")
|
||||||
|
@Data
|
||||||
|
public class OvertimeApplicationSaveReqVO {
|
||||||
|
|
||||||
|
@Schema(description = "加班日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06-01")
|
||||||
|
@NotNull(message = "加班日期不能为空")
|
||||||
|
private LocalDate overtimeDate;
|
||||||
|
|
||||||
|
@Schema(description = "加班时长", requiredMode = Schema.RequiredMode.REQUIRED, example = "1天")
|
||||||
|
@NotBlank(message = "加班时长不能为空")
|
||||||
|
@Size(max = 30, message = "加班时长长度不能超过30个字符")
|
||||||
|
private String overtimeDuration;
|
||||||
|
|
||||||
|
@Schema(description = "加班原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "版本上线保障")
|
||||||
|
@NotBlank(message = "加班原因不能为空")
|
||||||
|
@Size(max = 500, message = "加班原因长度不能超过500个字符")
|
||||||
|
private String overtimeReason;
|
||||||
|
|
||||||
|
@Schema(description = "加班内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "处理上线验证和问题修复")
|
||||||
|
@NotBlank(message = "加班内容不能为空")
|
||||||
|
@Size(max = 1000, message = "加班内容长度不能超过1000个字符")
|
||||||
|
private String overtimeContent;
|
||||||
|
|
||||||
|
@Schema(description = "审核人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
|
||||||
|
@NotNull(message = "审核人不能为空")
|
||||||
|
private Long approverId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 加班申请状态动作 Request VO")
|
||||||
|
@Data
|
||||||
|
public class OvertimeApplicationStatusActionReqVO {
|
||||||
|
|
||||||
|
@Schema(description = "动作原因或审核意见。退回、撤销必填", example = "请补充加班内容")
|
||||||
|
@Size(max = 1000, message = "动作原因长度不能超过1000个字符")
|
||||||
|
private String reason;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 加班申请状态日志 Response VO")
|
||||||
|
@Data
|
||||||
|
public class OvertimeApplicationStatusLogRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "加班申请编号", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Long applicationId;
|
||||||
|
|
||||||
|
@Schema(description = "动作编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "approve")
|
||||||
|
private String actionType;
|
||||||
|
|
||||||
|
@Schema(description = "变更前状态", example = "pending")
|
||||||
|
private String fromStatus;
|
||||||
|
|
||||||
|
@Schema(description = "变更后状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "approved")
|
||||||
|
private String toStatus;
|
||||||
|
|
||||||
|
@Schema(description = "原因或审核意见")
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
@Schema(description = "操作人用户编号", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Long operatorUserId;
|
||||||
|
|
||||||
|
@Schema(description = "操作人名称", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String operatorName;
|
||||||
|
|
||||||
|
@Schema(description = "申请人姓名快照", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String applicantNameSnapshot;
|
||||||
|
|
||||||
|
@Schema(description = "加班日期快照", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDate overtimeDateSnapshot;
|
||||||
|
|
||||||
|
@Schema(description = "加班时长快照", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String overtimeDurationSnapshot;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@@ -46,14 +46,14 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@Operation(summary = "创建个人事项")
|
@Operation(summary = "创建个人事项")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_CREATE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_CREATE + "')")
|
||||||
public CommonResult<Long> create(@Valid @RequestBody PersonalItemSaveReqVO reqVO) {
|
public CommonResult<Long> create(@Valid @RequestBody PersonalItemSaveReqVO reqVO) {
|
||||||
return success(personalItemService.createItem(reqVO));
|
return success(personalItemService.createItem(reqVO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@Operation(summary = "更新个人事项")
|
@Operation(summary = "更新个人事项")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||||
public CommonResult<Boolean> update(@PathVariable("id") Long id,
|
public CommonResult<Boolean> update(@PathVariable("id") Long id,
|
||||||
@Valid @RequestBody PersonalItemSaveReqVO reqVO) {
|
@Valid @RequestBody PersonalItemSaveReqVO reqVO) {
|
||||||
personalItemService.updateItem(id, reqVO);
|
personalItemService.updateItem(id, reqVO);
|
||||||
@@ -62,21 +62,21 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
@Operation(summary = "获取个人事项详情")
|
@Operation(summary = "获取个人事项详情")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
||||||
public CommonResult<PersonalItemRespVO> get(@PathVariable("id") Long id) {
|
public CommonResult<PersonalItemRespVO> get(@PathVariable("id") Long id) {
|
||||||
return success(personalItemService.getItemRespVO(id));
|
return success(personalItemService.getItemRespVO(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/page")
|
@GetMapping("/page")
|
||||||
@Operation(summary = "获取个人事项分页")
|
@Operation(summary = "获取个人事项分页")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
||||||
public CommonResult<PageResult<PersonalItemRespVO>> page(@Valid PersonalItemPageReqVO reqVO) {
|
public CommonResult<PageResult<PersonalItemRespVO>> page(@Valid PersonalItemPageReqVO reqVO) {
|
||||||
return success(personalItemService.getItemRespVOPage(reqVO));
|
return success(personalItemService.getItemRespVOPage(reqVO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/change-status")
|
@PostMapping("/{id}/change-status")
|
||||||
@Operation(summary = "变更个人事项状态")
|
@Operation(summary = "变更个人事项状态")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_STATUS + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_STATUS + "')")
|
||||||
public CommonResult<Boolean> changeStatus(@PathVariable("id") Long id,
|
public CommonResult<Boolean> changeStatus(@PathVariable("id") Long id,
|
||||||
@Valid @RequestBody PersonalItemStatusActionReqVO reqVO) {
|
@Valid @RequestBody PersonalItemStatusActionReqVO reqVO) {
|
||||||
personalItemService.changeStatus(id, reqVO);
|
personalItemService.changeStatus(id, reqVO);
|
||||||
@@ -85,7 +85,7 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@PostMapping("/{id}/worklogs")
|
@PostMapping("/{id}/worklogs")
|
||||||
@Operation(summary = "新增个人事项工作日志")
|
@Operation(summary = "新增个人事项工作日志")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||||
public CommonResult<Long> createWorklog(@PathVariable("id") Long id,
|
public CommonResult<Long> createWorklog(@PathVariable("id") Long id,
|
||||||
@Valid @RequestBody TaskWorklogSaveReqVO reqVO) {
|
@Valid @RequestBody TaskWorklogSaveReqVO reqVO) {
|
||||||
return success(personalItemService.createWorklog(id, reqVO));
|
return success(personalItemService.createWorklog(id, reqVO));
|
||||||
@@ -93,7 +93,7 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@GetMapping("/{id}/worklogs")
|
@GetMapping("/{id}/worklogs")
|
||||||
@Operation(summary = "获取个人事项工作日志分页")
|
@Operation(summary = "获取个人事项工作日志分页")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
||||||
public CommonResult<PageResult<TaskWorklogRespVO>> getWorklogPage(@PathVariable("id") Long id,
|
public CommonResult<PageResult<TaskWorklogRespVO>> getWorklogPage(@PathVariable("id") Long id,
|
||||||
@Valid TaskWorklogPageReqVO reqVO) {
|
@Valid TaskWorklogPageReqVO reqVO) {
|
||||||
return success(personalItemService.getWorklogPage(id, reqVO));
|
return success(personalItemService.getWorklogPage(id, reqVO));
|
||||||
@@ -101,7 +101,7 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@PutMapping("/{id}/worklogs/{worklogId}")
|
@PutMapping("/{id}/worklogs/{worklogId}")
|
||||||
@Operation(summary = "修改个人事项工作日志")
|
@Operation(summary = "修改个人事项工作日志")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||||
public CommonResult<Boolean> updateWorklog(@PathVariable("id") Long id,
|
public CommonResult<Boolean> updateWorklog(@PathVariable("id") Long id,
|
||||||
@PathVariable("worklogId") Long worklogId,
|
@PathVariable("worklogId") Long worklogId,
|
||||||
@Valid @RequestBody TaskWorklogSaveReqVO reqVO) {
|
@Valid @RequestBody TaskWorklogSaveReqVO reqVO) {
|
||||||
@@ -111,7 +111,7 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@DeleteMapping("/{id}/worklogs/{worklogId}")
|
@DeleteMapping("/{id}/worklogs/{worklogId}")
|
||||||
@Operation(summary = "删除个人事项工作日志")
|
@Operation(summary = "删除个人事项工作日志")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||||
public CommonResult<Boolean> deleteWorklog(@PathVariable("id") Long id,
|
public CommonResult<Boolean> deleteWorklog(@PathVariable("id") Long id,
|
||||||
@PathVariable("worklogId") Long worklogId) {
|
@PathVariable("worklogId") Long worklogId) {
|
||||||
personalItemService.deleteWorklog(id, worklogId);
|
personalItemService.deleteWorklog(id, worklogId);
|
||||||
@@ -120,7 +120,7 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@DeleteMapping("/{id}/worklogs/delete-list")
|
@DeleteMapping("/{id}/worklogs/delete-list")
|
||||||
@Operation(summary = "批量删除个人事项工作日志")
|
@Operation(summary = "批量删除个人事项工作日志")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||||
public CommonResult<Boolean> deleteWorklogs(@PathVariable("id") Long id,
|
public CommonResult<Boolean> deleteWorklogs(@PathVariable("id") Long id,
|
||||||
@RequestParam("ids") List<Long> ids) {
|
@RequestParam("ids") List<Long> ids) {
|
||||||
personalItemService.deleteWorklogs(id, ids);
|
personalItemService.deleteWorklogs(id, ids);
|
||||||
@@ -129,7 +129,7 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@DeleteMapping("/delete")
|
@DeleteMapping("/delete")
|
||||||
@Operation(summary = "删除个人事项")
|
@Operation(summary = "删除个人事项")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_DELETE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_DELETE + "')")
|
||||||
public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
|
public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
|
||||||
personalItemService.deleteItem(id);
|
personalItemService.deleteItem(id);
|
||||||
return success(true);
|
return success(true);
|
||||||
@@ -137,7 +137,7 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@DeleteMapping("/delete-list")
|
@DeleteMapping("/delete-list")
|
||||||
@Operation(summary = "批量删除个人事项")
|
@Operation(summary = "批量删除个人事项")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_DELETE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_DELETE + "')")
|
||||||
public CommonResult<Boolean> deleteList(@RequestParam("ids") List<Long> ids) {
|
public CommonResult<Boolean> deleteList(@RequestParam("ids") List<Long> ids) {
|
||||||
personalItemService.deleteItems(ids);
|
personalItemService.deleteItems(ids);
|
||||||
return success(true);
|
return success(true);
|
||||||
@@ -145,7 +145,7 @@ public class PersonalItemController {
|
|||||||
|
|
||||||
@PostMapping("/relate-execution")
|
@PostMapping("/relate-execution")
|
||||||
@Operation(summary = "批量个人事项关联执行")
|
@Operation(summary = "批量个人事项关联执行")
|
||||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
//@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||||
public CommonResult<Boolean> relateExecution(@RequestParam("itemIds") List<Long> itemIds,
|
public CommonResult<Boolean> relateExecution(@RequestParam("itemIds") List<Long> itemIds,
|
||||||
@RequestParam("executionId") Long executionId) {
|
@RequestParam("executionId") Long executionId) {
|
||||||
personalItemService.relateExecution(itemIds, executionId);
|
personalItemService.relateExecution(itemIds, executionId);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.njcn.rdms.module.project.controller.admin.project.execution;
|
package com.njcn.rdms.module.project.controller.admin.project.execution;
|
||||||
|
|
||||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionOwnerChangeReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionOwnerChangeReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO;
|
||||||
@@ -65,6 +66,11 @@ public class ProjectExecutionController {
|
|||||||
@Operation(summary = "获取执行分页")
|
@Operation(summary = "获取执行分页")
|
||||||
public CommonResult<PageResult<ProjectExecutionRespVO>> getExecutionPage(@PathVariable("projectId") Long projectId,
|
public CommonResult<PageResult<ProjectExecutionRespVO>> getExecutionPage(@PathVariable("projectId") Long projectId,
|
||||||
@Valid ProjectExecutionPageReqVO reqVO) {
|
@Valid ProjectExecutionPageReqVO reqVO) {
|
||||||
|
// 前端用负数 pageSize 表示"查询全部",统一归一为框架约定的 PAGE_SIZE_NONE(-1),
|
||||||
|
// 走 BaseMapperX.selectPage 的不分页短路;@Max(200) 仅拦上界,负数不会被 validator 卡。
|
||||||
|
if (reqVO.getPageSize() != null && reqVO.getPageSize() < 0) {
|
||||||
|
reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||||
|
}
|
||||||
return success(projectExecutionService.getExecutionRespVOPage(projectId, reqVO));
|
return success(projectExecutionService.getExecutionRespVOPage(projectId, reqVO));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ public class ProjectExecutionPageReqVO extends PageParam {
|
|||||||
@Size(max = 32, message = "执行类型长度不能超过32个字符")
|
@Size(max = 32, message = "执行类型长度不能超过32个字符")
|
||||||
private String executionType;
|
private String executionType;
|
||||||
|
|
||||||
@Schema(description = "执行负责人用户编号", example = "3001")
|
@Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一;不传 = 项目内全部执行")
|
||||||
|
private Long involveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一", example = "3001")
|
||||||
private Long ownerId;
|
private Long ownerId;
|
||||||
|
|
||||||
@Schema(description = "执行状态编码", example = "pending")
|
@Schema(description = "执行状态编码", example = "pending")
|
||||||
@@ -38,4 +41,8 @@ public class ProjectExecutionPageReqVO extends PageParam {
|
|||||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||||
private LocalDateTime[] updateTime;
|
private LocalDateTime[] updateTime;
|
||||||
|
|
||||||
|
@Schema(description = "截止时间范围 chip:overdue(逾期)/ today(今天到期)/ thisWeek(本周到期);" +
|
||||||
|
"基于 plannedEndDate 且排除终态执行;不传 = 不按截止时间过滤", example = "overdue")
|
||||||
|
private String dueRange;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,18 @@ public class ProjectExecutionStatusBoardReqVO {
|
|||||||
@Size(max = 32, message = "执行类型长度不能超过32个字符")
|
@Size(max = 32, message = "执行类型长度不能超过32个字符")
|
||||||
private String executionType;
|
private String executionType;
|
||||||
|
|
||||||
@Schema(description = "执行负责人用户编号", example = "3001")
|
@Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一;不传 = 项目内全部执行")
|
||||||
|
private Long involveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一", example = "3001")
|
||||||
private Long ownerId;
|
private Long ownerId;
|
||||||
|
|
||||||
@Schema(description = "更新时间", example = "[2026-05-01 00:00:00, 2026-05-31 23:59:59]")
|
@Schema(description = "更新时间", example = "[2026-05-01 00:00:00, 2026-05-31 23:59:59]")
|
||||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||||
private LocalDateTime[] updateTime;
|
private LocalDateTime[] updateTime;
|
||||||
|
|
||||||
|
@Schema(description = "截止时间范围 chip:overdue(逾期)/ today(今天到期)/ thisWeek(本周到期);" +
|
||||||
|
"基于 plannedEndDate 且排除终态执行;不传 = 不按截止时间过滤", example = "overdue")
|
||||||
|
private String dueRange;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.task;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskBoardPageRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateBoardPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregatePageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateStatusBoardReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskSummaryReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskSummaryRespVO;
|
||||||
|
import com.njcn.rdms.module.project.service.project.task.ProjectTaskAggregateService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
|
@Tag(name = "管理后台 - 项目级跨执行任务查询")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/project/project/{projectId}/tasks")
|
||||||
|
@Validated
|
||||||
|
public class ProjectTaskAggregateController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ProjectTaskAggregateService projectTaskAggregateService;
|
||||||
|
|
||||||
|
@GetMapping("/page")
|
||||||
|
@Operation(summary = "获取项目级跨执行任务分页")
|
||||||
|
public CommonResult<PageResult<ProjectTaskRespVO>> getTaskPage(
|
||||||
|
@PathVariable("projectId") Long projectId,
|
||||||
|
@Valid ProjectTaskAggregatePageReqVO reqVO) {
|
||||||
|
return success(projectTaskAggregateService.getAggregateTaskPage(projectId, reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/status-board")
|
||||||
|
@Operation(summary = "获取项目级跨执行任务状态看板")
|
||||||
|
public CommonResult<ProjectTaskStatusBoardRespVO> getTaskStatusBoard(
|
||||||
|
@PathVariable("projectId") Long projectId,
|
||||||
|
@Valid ProjectTaskAggregateStatusBoardReqVO reqVO) {
|
||||||
|
return success(projectTaskAggregateService.getAggregateTaskStatusBoard(projectId, reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/board-page")
|
||||||
|
@Operation(summary = "获取项目级跨执行任务看板分页")
|
||||||
|
public CommonResult<ProjectTaskBoardPageRespVO> getTaskBoardPage(
|
||||||
|
@PathVariable("projectId") Long projectId,
|
||||||
|
@Valid ProjectTaskAggregateBoardPageReqVO reqVO) {
|
||||||
|
return success(projectTaskAggregateService.getAggregateTaskBoardPage(projectId, reqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/summary")
|
||||||
|
@Operation(summary = "获取项目任务今日小条(involveUserId 控制是否限定 owner / 活跃协办)")
|
||||||
|
public CommonResult<ProjectTaskSummaryRespVO> getTaskSummary(
|
||||||
|
@PathVariable("projectId") Long projectId,
|
||||||
|
@Valid ProjectTaskSummaryReqVO reqVO) {
|
||||||
|
return success(projectTaskAggregateService.getAggregateTaskSummary(projectId, reqVO));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,7 +36,10 @@ public class ProjectTaskBoardPageReqVO extends PageParam {
|
|||||||
@Schema(description = "父任务编号", example = "9001")
|
@Schema(description = "父任务编号", example = "9001")
|
||||||
private Long parentTaskId;
|
private Long parentTaskId;
|
||||||
|
|
||||||
@Schema(description = "任务负责人用户编号", example = "3002")
|
@Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一;不传 = 执行下全部任务")
|
||||||
|
private Long involveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一", example = "3002")
|
||||||
private Long ownerId;
|
private Long ownerId;
|
||||||
|
|
||||||
@Schema(description = "更新时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
|
@Schema(description = "更新时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ public class ProjectTaskPageReqVO extends PageParam {
|
|||||||
@Schema(description = "父任务编号")
|
@Schema(description = "父任务编号")
|
||||||
private Long parentTaskId;
|
private Long parentTaskId;
|
||||||
|
|
||||||
@Schema(description = "任务负责人用户编号", example = "3002")
|
@Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一;不传 = 执行下全部任务")
|
||||||
|
private Long involveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一", example = "3002")
|
||||||
private Long ownerId;
|
private Long ownerId;
|
||||||
|
|
||||||
@Schema(description = "任务状态编码", example = "pending")
|
@Schema(description = "任务状态编码", example = "pending")
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ public class ProjectTaskRespVO {
|
|||||||
private Long projectId;
|
private Long projectId;
|
||||||
@Schema(description = "所属执行编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5001")
|
@Schema(description = "所属执行编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5001")
|
||||||
private Long executionId;
|
private Long executionId;
|
||||||
|
@Schema(description = "所属执行名称", example = "迭代 V1.0")
|
||||||
|
private String executionName;
|
||||||
|
@Schema(description = "所属执行状态码")
|
||||||
|
private String executionStatusCode;
|
||||||
@Schema(description = "所属执行关联的项目需求编号(service 层批量回填)")
|
@Schema(description = "所属执行关联的项目需求编号(service 层批量回填)")
|
||||||
private Long projectRequirementId;
|
private Long projectRequirementId;
|
||||||
@Schema(description = "项目需求名称(service 层批量回填,避免 N+1)", example = "前端联调-需求A")
|
@Schema(description = "项目需求名称(service 层批量回填,避免 N+1)", example = "前端联调-需求A")
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ public class ProjectTaskStatusBoardReqVO {
|
|||||||
@Schema(description = "父任务编号", example = "9001")
|
@Schema(description = "父任务编号", example = "9001")
|
||||||
private Long parentTaskId;
|
private Long parentTaskId;
|
||||||
|
|
||||||
@Schema(description = "任务负责人用户编号", example = "3002")
|
@Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一;不传 = 执行下全部任务")
|
||||||
|
private Long involveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一", example = "3002")
|
||||||
private Long ownerId;
|
private Long ownerId;
|
||||||
|
|
||||||
@Schema(description = "更新时间", example = "[2026-05-01 00:00:00, 2026-05-31 23:59:59]")
|
@Schema(description = "更新时间", example = "[2026-05-01 00:00:00, 2026-05-31 23:59:59]")
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 项目级跨执行任务看板分页 Request VO")
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ProjectTaskAggregateBoardPageReqVO extends PageParam {
|
||||||
|
|
||||||
|
@Schema(description = "任务名称模糊匹配关键字")
|
||||||
|
private String keyword;
|
||||||
|
|
||||||
|
@Schema(description = "限定执行 id 列表;空数组 = 明确返空;不传 = 项目内全部执行")
|
||||||
|
private List<Long> executionIds;
|
||||||
|
|
||||||
|
@Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤")
|
||||||
|
private List<String> executionStatusCodes;
|
||||||
|
|
||||||
|
@Schema(description = "我参与语义;与 ownerId 二选一")
|
||||||
|
private Long involveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "执行成员语义:该 userId 是执行 owner 或活跃执行协办;过滤其参与的执行下的任务。与 involveUserId(任务成员)正交,可同传;用户未参与任何执行时返空")
|
||||||
|
private Long executionInvolveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "仅作为 owner 匹配;与 involveUserId 二选一")
|
||||||
|
private Long ownerId;
|
||||||
|
|
||||||
|
@Schema(description = "限定状态列(板上显示哪些列);空 / 不传 = 字典全状态")
|
||||||
|
private List<String> statusCodes;
|
||||||
|
|
||||||
|
@Schema(description = "优先级")
|
||||||
|
@Size(max = 8)
|
||||||
|
private String priority;
|
||||||
|
|
||||||
|
@Schema(description = "父任务 id")
|
||||||
|
private Long parentTaskId;
|
||||||
|
|
||||||
|
@Schema(description = "到期范围 chip:overdue / today / thisWeek")
|
||||||
|
private String dueRange;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间范围")
|
||||||
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||||
|
private LocalDateTime[] updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 项目级跨执行任务分页 Request VO")
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ProjectTaskAggregatePageReqVO extends PageParam {
|
||||||
|
|
||||||
|
@Schema(description = "任务名称模糊匹配关键字")
|
||||||
|
private String keyword;
|
||||||
|
|
||||||
|
@Schema(description = "限定执行 id 列表;空数组 = 明确返空;不传 = 项目内全部执行")
|
||||||
|
private List<Long> executionIds;
|
||||||
|
|
||||||
|
@Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤")
|
||||||
|
private List<String> executionStatusCodes;
|
||||||
|
|
||||||
|
@Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;与 ownerId 二选一")
|
||||||
|
private Long involveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "执行成员语义:该 userId 是执行 owner 或活跃执行协办;过滤其参与的执行下的任务。与 involveUserId(任务成员)正交,可同传;用户未参与任何执行时返空")
|
||||||
|
private Long executionInvolveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "仅作为 owner 匹配(不含协办);与 involveUserId 二选一")
|
||||||
|
private Long ownerId;
|
||||||
|
|
||||||
|
@Schema(description = "状态码多选;空 / 不传 = 全部")
|
||||||
|
private List<String> statusCodes;
|
||||||
|
|
||||||
|
@Schema(description = "优先级字典 value (0~3)")
|
||||||
|
@Size(max = 8)
|
||||||
|
private String priority;
|
||||||
|
|
||||||
|
@Schema(description = "父任务 id(限定到某父任务下)")
|
||||||
|
private Long parentTaskId;
|
||||||
|
|
||||||
|
@Schema(description = "到期范围 chip:overdue / today / thisWeek")
|
||||||
|
private String dueRange;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间范围(2 长度数组)")
|
||||||
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||||
|
private LocalDateTime[] updateTime;
|
||||||
|
|
||||||
|
@Schema(description = "排序字段:plannedEndDate / priority / updateTime / createTime(默认 plannedEndDate)")
|
||||||
|
private String sortBy;
|
||||||
|
|
||||||
|
@Schema(description = "排序方向:asc / desc(默认 asc)")
|
||||||
|
private String sortOrder;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 项目级跨执行任务状态看板 Request VO(入参同 page 去掉分页 / statusCodes / sort)")
|
||||||
|
@Data
|
||||||
|
public class ProjectTaskAggregateStatusBoardReqVO {
|
||||||
|
|
||||||
|
@Schema(description = "任务名称模糊匹配关键字")
|
||||||
|
private String keyword;
|
||||||
|
|
||||||
|
@Schema(description = "限定执行 id 列表;空数组 = 明确返空;不传 = 项目内全部执行")
|
||||||
|
private List<Long> executionIds;
|
||||||
|
|
||||||
|
@Schema(description = "限定任务所属执行的状态码多选;空数组 = 明确返空;不传 = 不按执行状态过滤")
|
||||||
|
private List<String> executionStatusCodes;
|
||||||
|
|
||||||
|
@Schema(description = "我参与语义;与 ownerId 二选一")
|
||||||
|
private Long involveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "执行成员语义:该 userId 是执行 owner 或活跃执行协办;过滤其参与的执行下的任务。与 involveUserId(任务成员)正交,可同传;用户未参与任何执行时返空")
|
||||||
|
private Long executionInvolveUserId;
|
||||||
|
|
||||||
|
@Schema(description = "仅作为 owner 匹配;与 involveUserId 二选一")
|
||||||
|
private Long ownerId;
|
||||||
|
|
||||||
|
@Schema(description = "优先级")
|
||||||
|
@Size(max = 8)
|
||||||
|
private String priority;
|
||||||
|
|
||||||
|
@Schema(description = "父任务 id")
|
||||||
|
private Long parentTaskId;
|
||||||
|
|
||||||
|
@Schema(description = "到期范围 chip:overdue / today / thisWeek")
|
||||||
|
private String dueRange;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间范围")
|
||||||
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||||
|
private LocalDateTime[] updateTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 项目任务今日小条 Request VO")
|
||||||
|
@Data
|
||||||
|
public class ProjectTaskSummaryReqVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我参与语义:传入的 userId 是 owner 或活跃协办;不传 = 项目内全部任务。
|
||||||
|
* 切换"我参与 / 所有"由前端直接控制此字段是否携带,与其他读接口(page / status-board / board-page)契约一致。
|
||||||
|
*/
|
||||||
|
@Schema(description = "我参与语义:该 userId 是 owner 或活跃协办;不传 = 项目内全部")
|
||||||
|
private Long involveUserId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 项目任务今日小条 Response VO")
|
||||||
|
@Data
|
||||||
|
public class ProjectTaskSummaryRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "逾期任务数:计划完成日 < 今天,且任务状态非终态")
|
||||||
|
private Long overdue;
|
||||||
|
|
||||||
|
@Schema(description = "今日截止任务数:计划完成日 = 今天,且任务状态非终态")
|
||||||
|
private Long dueToday;
|
||||||
|
|
||||||
|
@Schema(description = "本周到期任务数:计划完成日 ∈ [本周一, 本周日],且任务状态非终态(与 chip thisWeek 过滤同口径)")
|
||||||
|
private Long dueThisWeek;
|
||||||
|
|
||||||
|
@Schema(description = "本周已完成任务数:actualEndDate ∈ [本周一, 今天],且任务状态 = completed")
|
||||||
|
private Long doneThisWeek;
|
||||||
|
|
||||||
|
@Schema(description = "服务器当日(YYYY-MM-DD,Asia/Shanghai)", example = "2026-05-22")
|
||||||
|
private LocalDate today;
|
||||||
|
|
||||||
|
@Schema(description = "服务器本周一(YYYY-MM-DD,Asia/Shanghai)", example = "2026-05-18")
|
||||||
|
private LocalDate weekStart;
|
||||||
|
|
||||||
|
@Schema(description = "服务器本周日(YYYY-MM-DD,Asia/Shanghai)", example = "2026-05-24")
|
||||||
|
private LocalDate weekEnd;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.njcn.rdms.module.project.dal.dataobject.overtime;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加班申请表。
|
||||||
|
*/
|
||||||
|
@TableName("rdms_overtime_application")
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class OvertimeApplicationDO extends BaseDO {
|
||||||
|
|
||||||
|
@TableId
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long applicantId;
|
||||||
|
|
||||||
|
private String applicantName;
|
||||||
|
|
||||||
|
private LocalDate overtimeDate;
|
||||||
|
|
||||||
|
private String overtimeDuration;
|
||||||
|
|
||||||
|
private String overtimeReason;
|
||||||
|
|
||||||
|
private String overtimeContent;
|
||||||
|
|
||||||
|
private Long approverId;
|
||||||
|
|
||||||
|
private String approverName;
|
||||||
|
|
||||||
|
private String statusCode;
|
||||||
|
|
||||||
|
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||||
|
private String approvalComment;
|
||||||
|
|
||||||
|
private LocalDateTime submitTime;
|
||||||
|
|
||||||
|
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||||
|
private LocalDateTime approvalTime;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.njcn.rdms.module.project.dal.dataobject.overtime;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加班申请状态日志表。
|
||||||
|
*/
|
||||||
|
@TableName("rdms_overtime_application_status_log")
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class OvertimeApplicationStatusLogDO extends BaseDO {
|
||||||
|
|
||||||
|
@TableId
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long applicationId;
|
||||||
|
|
||||||
|
private String actionType;
|
||||||
|
|
||||||
|
private String fromStatus;
|
||||||
|
|
||||||
|
private String toStatus;
|
||||||
|
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
private Long operatorUserId;
|
||||||
|
|
||||||
|
private String operatorName;
|
||||||
|
|
||||||
|
private String applicantNameSnapshot;
|
||||||
|
|
||||||
|
private LocalDate overtimeDateSnapshot;
|
||||||
|
|
||||||
|
private String overtimeDurationSnapshot;
|
||||||
|
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.njcn.rdms.module.project.dal.mysql.overtime;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
|
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||||
|
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||||
|
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationDO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface OvertimeApplicationMapper extends BaseMapperX<OvertimeApplicationDO> {
|
||||||
|
|
||||||
|
default PageResult<OvertimeApplicationDO> selectMyPage(Long applicantId, OvertimeApplicationPageReqVO reqVO) {
|
||||||
|
LambdaQueryWrapperX<OvertimeApplicationDO> queryWrapper = buildPageQuery(reqVO);
|
||||||
|
queryWrapper.eq(OvertimeApplicationDO::getApplicantId, applicantId);
|
||||||
|
queryWrapper.orderByDesc(OvertimeApplicationDO::getSubmitTime)
|
||||||
|
.orderByDesc(OvertimeApplicationDO::getId);
|
||||||
|
return selectPage(reqVO, queryWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
default PageResult<OvertimeApplicationDO> selectApprovalPage(Long approverId, OvertimeApplicationPageReqVO reqVO) {
|
||||||
|
LambdaQueryWrapperX<OvertimeApplicationDO> queryWrapper = buildPageQuery(reqVO);
|
||||||
|
queryWrapper.eq(OvertimeApplicationDO::getApproverId, approverId);
|
||||||
|
queryWrapper.orderByDesc(OvertimeApplicationDO::getSubmitTime)
|
||||||
|
.orderByDesc(OvertimeApplicationDO::getId);
|
||||||
|
return selectPage(reqVO, queryWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
default OvertimeApplicationDO selectByIdAndApplicantId(Long id, Long applicantId) {
|
||||||
|
return selectOne(new LambdaQueryWrapperX<OvertimeApplicationDO>()
|
||||||
|
.eq(OvertimeApplicationDO::getId, id)
|
||||||
|
.eq(OvertimeApplicationDO::getApplicantId, applicantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
default int updateByIdAndStatus(OvertimeApplicationDO update, Long id, String fromStatus) {
|
||||||
|
return update(update, new LambdaQueryWrapperX<OvertimeApplicationDO>()
|
||||||
|
.eq(OvertimeApplicationDO::getId, id)
|
||||||
|
.eq(OvertimeApplicationDO::getStatusCode, fromStatus));
|
||||||
|
}
|
||||||
|
|
||||||
|
default int updateByIdAndStatusAndApplicantId(OvertimeApplicationDO update, Long id, String fromStatus,
|
||||||
|
Long applicantId) {
|
||||||
|
return update(update, new LambdaQueryWrapperX<OvertimeApplicationDO>()
|
||||||
|
.eq(OvertimeApplicationDO::getId, id)
|
||||||
|
.eq(OvertimeApplicationDO::getStatusCode, fromStatus)
|
||||||
|
.eq(OvertimeApplicationDO::getApplicantId, applicantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
default int updateByIdAndStatusesAndApplicantId(OvertimeApplicationDO update, Long id,
|
||||||
|
Collection<String> fromStatuses, Long applicantId) {
|
||||||
|
return update(update, new LambdaQueryWrapperX<OvertimeApplicationDO>()
|
||||||
|
.eq(OvertimeApplicationDO::getId, id)
|
||||||
|
.in(OvertimeApplicationDO::getStatusCode, fromStatuses)
|
||||||
|
.eq(OvertimeApplicationDO::getApplicantId, applicantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LambdaQueryWrapperX<OvertimeApplicationDO> buildPageQuery(OvertimeApplicationPageReqVO reqVO) {
|
||||||
|
LambdaQueryWrapperX<OvertimeApplicationDO> queryWrapper = new LambdaQueryWrapperX<>();
|
||||||
|
queryWrapper.likeIfPresent(OvertimeApplicationDO::getApplicantName, reqVO.getApplicantName())
|
||||||
|
.eqIfPresent(OvertimeApplicationDO::getApproverId, reqVO.getApproverId())
|
||||||
|
.likeIfPresent(OvertimeApplicationDO::getApproverName, reqVO.getApproverName())
|
||||||
|
.eqIfPresent(OvertimeApplicationDO::getStatusCode, reqVO.getStatusCode())
|
||||||
|
.betweenIfPresent(OvertimeApplicationDO::getOvertimeDate, reqVO.getOvertimeDate())
|
||||||
|
.betweenIfPresent(BaseDO::getCreateTime, reqVO.getCreateTime());
|
||||||
|
if (StringUtils.hasText(reqVO.getKeyword())) {
|
||||||
|
queryWrapper.and(wrapper -> wrapper.like(OvertimeApplicationDO::getOvertimeReason, reqVO.getKeyword())
|
||||||
|
.or()
|
||||||
|
.like(OvertimeApplicationDO::getOvertimeContent, reqVO.getKeyword()));
|
||||||
|
}
|
||||||
|
return queryWrapper;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.njcn.rdms.module.project.dal.mysql.overtime;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||||
|
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationStatusLogDO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface OvertimeApplicationStatusLogMapper extends BaseMapperX<OvertimeApplicationStatusLogDO> {
|
||||||
|
|
||||||
|
default List<OvertimeApplicationStatusLogDO> selectListByApplicationId(Long applicationId) {
|
||||||
|
return selectList(new LambdaQueryWrapperX<OvertimeApplicationStatusLogDO>()
|
||||||
|
.eq(OvertimeApplicationStatusLogDO::getApplicationId, applicationId)
|
||||||
|
.orderByDesc(OvertimeApplicationStatusLogDO::getCreateTime)
|
||||||
|
.orderByDesc(OvertimeApplicationStatusLogDO::getId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,4 +73,14 @@ public interface ProjectMapper extends BaseMapperX<ProjectDO> {
|
|||||||
.eq(ProjectDO::getStatusCode, statusCode));
|
.eq(ProjectDO::getStatusCode, statusCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅更新项目 progressRate,不动其他字段(避免污染 lastStatusReason 等)。
|
||||||
|
*/
|
||||||
|
default int updateProgressRateById(Long id, java.math.BigDecimal progressRate) {
|
||||||
|
ProjectDO update = new ProjectDO();
|
||||||
|
update.setProgressRate(progressRate);
|
||||||
|
return update(update, new LambdaQueryWrapperX<ProjectDO>()
|
||||||
|
.eq(ProjectDO::getId, id));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
|||||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionAssigneeDO;
|
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionAssigneeDO;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
import org.apache.ibatis.annotations.Select;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -50,23 +48,6 @@ public interface ExecutionAssigneeMapper extends BaseMapperX<ExecutionAssigneeDO
|
|||||||
.isNull(ExecutionAssigneeDO::getRemovedAt));
|
.isNull(ExecutionAssigneeDO::getRemovedAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 查 userId 当前在指定项目下,活跃协办的所有执行 ID(removed_at IS NULL)。
|
|
||||||
* 走 JOIN 是因为 execution_assignee 表没有 project_id 冗余字段。
|
|
||||||
* 用于 VisibilityScopeResolver 收集"我是执行协办人"的 scope 来源。
|
|
||||||
*/
|
|
||||||
@Select("""
|
|
||||||
SELECT a.execution_id
|
|
||||||
FROM rdms_execution_assignee a
|
|
||||||
JOIN rdms_project_execution e ON e.id = a.execution_id AND e.deleted = b'0'
|
|
||||||
WHERE a.deleted = b'0'
|
|
||||||
AND a.removed_at IS NULL
|
|
||||||
AND e.project_id = #{projectId}
|
|
||||||
AND a.user_id = #{userId}
|
|
||||||
""")
|
|
||||||
List<Long> selectActiveExecutionIdsByProjectIdAndUserId(@Param("projectId") Long projectId,
|
|
||||||
@Param("userId") Long userId);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按 execution_id 批量软删执行协办(含已 removed 的历史段)。
|
* 按 execution_id 批量软删执行协办(含已 removed 的历史段)。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.njcn.rdms.module.project.dal.mysql.project.execution;
|
package com.njcn.rdms.module.project.dal.mysql.project.execution;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||||
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||||
@@ -7,12 +9,11 @@ import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
|||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusBoardReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusBoardReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO;
|
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO;
|
||||||
import com.njcn.rdms.module.project.service.project.permission.VisibilityScope;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -33,44 +34,97 @@ public interface ProjectExecutionMapper extends BaseMapperX<ProjectExecutionDO>
|
|||||||
}
|
}
|
||||||
|
|
||||||
default PageResult<ProjectExecutionDO> selectPageByProjectId(Long projectId,
|
default PageResult<ProjectExecutionDO> selectPageByProjectId(Long projectId,
|
||||||
VisibilityScope scope,
|
ProjectExecutionPageReqVO reqVO,
|
||||||
ProjectExecutionPageReqVO reqVO) {
|
List<String> terminalStatusCodes,
|
||||||
// 可见性短路:非 seesAll 且无任何可见执行 → 空页,避免后续 IN () SQL
|
LocalDate today,
|
||||||
if (!scope.seesAll() && scope.executionIds().isEmpty()) {
|
LocalDate weekStart,
|
||||||
return PageResult.empty();
|
LocalDate weekEnd) {
|
||||||
}
|
Page<ProjectExecutionDO> page = new Page<>(reqVO.getPageNo(), reqVO.getPageSize());
|
||||||
LambdaQueryWrapperX<ProjectExecutionDO> queryWrapper = new LambdaQueryWrapperX<>();
|
IPage<ProjectExecutionDO> ipage = doSelectPageByProjectId(
|
||||||
queryWrapper.eq(ProjectExecutionDO::getProjectId, projectId);
|
projectId, reqVO, terminalStatusCodes, today, weekStart, weekEnd, page);
|
||||||
queryWrapper.eqIfPresent(ProjectExecutionDO::getExecutionType, reqVO.getExecutionType());
|
return new PageResult<>(ipage.getRecords(), ipage.getTotal());
|
||||||
queryWrapper.eqIfPresent(ProjectExecutionDO::getOwnerId, reqVO.getOwnerId());
|
|
||||||
queryWrapper.eqIfPresent(ProjectExecutionDO::getStatusCode, reqVO.getStatusCode());
|
|
||||||
queryWrapper.eqIfPresent(ProjectExecutionDO::getPriority, reqVO.getPriority());
|
|
||||||
queryWrapper.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime());
|
|
||||||
queryWrapper.orderByAsc(ProjectExecutionDO::getPriority);
|
|
||||||
queryWrapper.orderByDesc(BaseDO::getCreateTime);
|
|
||||||
queryWrapper.orderByDesc(ProjectExecutionDO::getId);
|
|
||||||
if (StringUtils.hasText(reqVO.getKeyword())) {
|
|
||||||
queryWrapper.and(wrapper -> wrapper.like(ProjectExecutionDO::getExecutionName, reqVO.getKeyword()));
|
|
||||||
}
|
|
||||||
if (!scope.seesAll()) {
|
|
||||||
queryWrapper.in(ProjectExecutionDO::getId, scope.executionIds());
|
|
||||||
}
|
|
||||||
return selectPage(reqVO, queryWrapper);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查 userId 在指定项目下,作为 owner 的所有执行 ID。
|
* 项目下执行分页查询。
|
||||||
* 用于 VisibilityScopeResolver 收集"我是执行负责人"的 scope 来源。
|
* <p>SQL 用 @Select 直接控制,主表以别名 t 暴露,EXISTS 子查询用 t.id 关联;
|
||||||
|
* 避免 LambdaWrapper + .apply 嵌入裸 SQL 时依赖 "MyBatis-Plus 不给主表加别名" 这一实现细节。
|
||||||
|
* 与任务侧 {@code ProjectTaskMapper.selectAggregatePageByProjectId} 同款风格。
|
||||||
|
*
|
||||||
|
* <p>involveUserId 非空时附加 (owner_id = involveUserId OR 活跃协办含 involveUserId);
|
||||||
|
* 否则该过滤分支跳过("看项目下全部")。
|
||||||
*/
|
*/
|
||||||
default List<Long> selectIdsByProjectIdAndOwnerId(Long projectId, Long userId) {
|
@Select("""
|
||||||
return selectList(new LambdaQueryWrapperX<ProjectExecutionDO>()
|
<script>
|
||||||
.select(ProjectExecutionDO::getId)
|
SELECT t.*
|
||||||
.eq(ProjectExecutionDO::getProjectId, projectId)
|
FROM rdms_project_execution t
|
||||||
.eq(ProjectExecutionDO::getOwnerId, userId))
|
<where>
|
||||||
.stream()
|
t.deleted = b'0'
|
||||||
.map(ProjectExecutionDO::getId)
|
AND t.project_id = #{projectId}
|
||||||
.toList();
|
<if test="reqVO.executionType != null and reqVO.executionType != ''">
|
||||||
}
|
AND t.execution_type = #{reqVO.executionType}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.ownerId != null">
|
||||||
|
AND t.owner_id = #{reqVO.ownerId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.statusCode != null and reqVO.statusCode != ''">
|
||||||
|
AND t.status_code = #{reqVO.statusCode}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.priority != null and reqVO.priority != ''">
|
||||||
|
AND t.priority = #{reqVO.priority}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.updateTime != null and reqVO.updateTime.length == 2">
|
||||||
|
AND t.update_time BETWEEN #{reqVO.updateTime[0]} AND #{reqVO.updateTime[1]}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.keyword != null and reqVO.keyword != ''">
|
||||||
|
AND t.execution_name LIKE CONCAT('%', #{reqVO.keyword}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.involveUserId != null">
|
||||||
|
AND (
|
||||||
|
t.owner_id = #{reqVO.involveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_execution_assignee a
|
||||||
|
WHERE a.execution_id = t.id
|
||||||
|
AND a.user_id = #{reqVO.involveUserId}
|
||||||
|
AND a.removed_at IS NULL
|
||||||
|
AND a.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<!-- 截止时间范围 chip:基于 planned_end_date,三个桶均排除终态(对齐任务 summary 口径) -->
|
||||||
|
<if test="reqVO.dueRange == 'overdue'">
|
||||||
|
AND t.planned_end_date < #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'today'">
|
||||||
|
AND t.planned_end_date = #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'thisWeek'">
|
||||||
|
AND t.planned_end_date BETWEEN #{weekStart} AND #{weekEnd}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY t.priority ASC, t.create_time DESC, t.id DESC
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
IPage<ProjectExecutionDO> doSelectPageByProjectId(
|
||||||
|
@Param("projectId") Long projectId,
|
||||||
|
@Param("reqVO") ProjectExecutionPageReqVO reqVO,
|
||||||
|
@Param("terminalStatusCodes") List<String> terminalStatusCodes,
|
||||||
|
@Param("today") LocalDate today,
|
||||||
|
@Param("weekStart") LocalDate weekStart,
|
||||||
|
@Param("weekEnd") LocalDate weekEnd,
|
||||||
|
Page<ProjectExecutionDO> page);
|
||||||
|
|
||||||
default List<ProjectExecutionDO> selectListByOwnerId(Long ownerId) {
|
default List<ProjectExecutionDO> selectListByOwnerId(Long ownerId) {
|
||||||
return selectList(new LambdaQueryWrapperX<ProjectExecutionDO>()
|
return selectList(new LambdaQueryWrapperX<ProjectExecutionDO>()
|
||||||
@@ -90,28 +144,86 @@ public interface ProjectExecutionMapper extends BaseMapperX<ProjectExecutionDO>
|
|||||||
}
|
}
|
||||||
|
|
||||||
default Integer countByProjectIdAndStatusCode(Long projectId,
|
default Integer countByProjectIdAndStatusCode(Long projectId,
|
||||||
VisibilityScope scope,
|
|
||||||
ProjectExecutionStatusBoardReqVO reqVO,
|
ProjectExecutionStatusBoardReqVO reqVO,
|
||||||
String statusCode) {
|
String statusCode,
|
||||||
// 可见性短路:非 seesAll 且无任何可见执行 → 计数 0
|
List<String> terminalStatusCodes,
|
||||||
if (!scope.seesAll() && scope.executionIds().isEmpty()) {
|
LocalDate today,
|
||||||
return 0;
|
LocalDate weekStart,
|
||||||
}
|
LocalDate weekEnd) {
|
||||||
LambdaQueryWrapperX<ProjectExecutionDO> queryWrapper = new LambdaQueryWrapperX<ProjectExecutionDO>()
|
return Math.toIntExact(doCountByProjectIdAndStatusCode(
|
||||||
.eq(ProjectExecutionDO::getProjectId, projectId)
|
projectId, reqVO, statusCode, terminalStatusCodes, today, weekStart, weekEnd));
|
||||||
.eq(ProjectExecutionDO::getStatusCode, statusCode)
|
|
||||||
.eqIfPresent(ProjectExecutionDO::getExecutionType, reqVO.getExecutionType())
|
|
||||||
.eqIfPresent(ProjectExecutionDO::getOwnerId, reqVO.getOwnerId())
|
|
||||||
.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime());
|
|
||||||
if (StringUtils.hasText(reqVO.getKeyword())) {
|
|
||||||
queryWrapper.and(wrapper -> wrapper.like(ProjectExecutionDO::getExecutionName, reqVO.getKeyword()));
|
|
||||||
}
|
|
||||||
if (!scope.seesAll()) {
|
|
||||||
queryWrapper.in(ProjectExecutionDO::getId, scope.executionIds());
|
|
||||||
}
|
|
||||||
return Math.toIntExact(selectCount(queryWrapper));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目下指定状态的执行计数(与 doSelectPageByProjectId 同款过滤口径)。
|
||||||
|
* 同上:用 @Select 显式表别名 t 替代 LambdaWrapper + .apply EXISTS 写法。
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM rdms_project_execution t
|
||||||
|
<where>
|
||||||
|
t.deleted = b'0'
|
||||||
|
AND t.project_id = #{projectId}
|
||||||
|
AND t.status_code = #{statusCode}
|
||||||
|
<if test="reqVO.executionType != null and reqVO.executionType != ''">
|
||||||
|
AND t.execution_type = #{reqVO.executionType}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.ownerId != null">
|
||||||
|
AND t.owner_id = #{reqVO.ownerId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.updateTime != null and reqVO.updateTime.length == 2">
|
||||||
|
AND t.update_time BETWEEN #{reqVO.updateTime[0]} AND #{reqVO.updateTime[1]}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.keyword != null and reqVO.keyword != ''">
|
||||||
|
AND t.execution_name LIKE CONCAT('%', #{reqVO.keyword}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.involveUserId != null">
|
||||||
|
AND (
|
||||||
|
t.owner_id = #{reqVO.involveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_execution_assignee a
|
||||||
|
WHERE a.execution_id = t.id
|
||||||
|
AND a.user_id = #{reqVO.involveUserId}
|
||||||
|
AND a.removed_at IS NULL
|
||||||
|
AND a.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<!-- 截止时间范围 chip:基于 planned_end_date,三个桶均排除终态(对齐任务 summary 口径) -->
|
||||||
|
<if test="reqVO.dueRange == 'overdue'">
|
||||||
|
AND t.planned_end_date < #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'today'">
|
||||||
|
AND t.planned_end_date = #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'thisWeek'">
|
||||||
|
AND t.planned_end_date BETWEEN #{weekStart} AND #{weekEnd}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
Long doCountByProjectIdAndStatusCode(
|
||||||
|
@Param("projectId") Long projectId,
|
||||||
|
@Param("reqVO") ProjectExecutionStatusBoardReqVO reqVO,
|
||||||
|
@Param("statusCode") String statusCode,
|
||||||
|
@Param("terminalStatusCodes") List<String> terminalStatusCodes,
|
||||||
|
@Param("today") LocalDate today,
|
||||||
|
@Param("weekStart") LocalDate weekStart,
|
||||||
|
@Param("weekEnd") LocalDate weekEnd);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TD-016:按 projectRequirementIds 批量聚合承接执行的平均进度,避免列表 N+1。
|
* TD-016:按 projectRequirementIds 批量聚合承接执行的平均进度,避免列表 N+1。
|
||||||
* <p>口径:仅统计 deleted = 0 + status_code NOT IN(excludedStatusCodes) 的执行;
|
* <p>口径:仅统计 deleted = 0 + status_code NOT IN(excludedStatusCodes) 的执行;
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
package com.njcn.rdms.module.project.dal.mysql.project.task;
|
package com.njcn.rdms.module.project.dal.mysql.project.task;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
|
||||||
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskPageReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregatePageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateStatusBoardReqVO;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
|
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
|
||||||
import com.njcn.rdms.module.project.service.project.permission.VisibilityScope;
|
import lombok.Data;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
@@ -31,33 +33,68 @@ public interface ProjectTaskMapper extends BaseMapperX<ProjectTaskDO> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
default PageResult<ProjectTaskDO> selectPageByExecutionId(Long projectId, Long executionId,
|
default PageResult<ProjectTaskDO> selectPageByExecutionId(Long projectId, Long executionId,
|
||||||
VisibilityScope scope,
|
|
||||||
ProjectTaskPageReqVO reqVO) {
|
ProjectTaskPageReqVO reqVO) {
|
||||||
// 可见性短路:非 seesAll 且无任何可见任务 → 空页
|
Page<ProjectTaskDO> page = new Page<>(reqVO.getPageNo(), reqVO.getPageSize());
|
||||||
if (!scope.seesAll() && scope.taskIds().isEmpty()) {
|
IPage<ProjectTaskDO> ipage = doSelectPageByExecutionId(projectId, executionId, reqVO, page);
|
||||||
return PageResult.empty();
|
return new PageResult<>(ipage.getRecords(), ipage.getTotal());
|
||||||
}
|
|
||||||
LambdaQueryWrapperX<ProjectTaskDO> queryWrapper = new LambdaQueryWrapperX<>();
|
|
||||||
queryWrapper.eq(ProjectTaskDO::getProjectId, projectId);
|
|
||||||
queryWrapper.eq(ProjectTaskDO::getExecutionId, executionId);
|
|
||||||
queryWrapper.eqIfPresent(ProjectTaskDO::getParentTaskId, reqVO.getParentTaskId());
|
|
||||||
queryWrapper.eqIfPresent(ProjectTaskDO::getOwnerId, reqVO.getOwnerId());
|
|
||||||
queryWrapper.eqIfPresent(ProjectTaskDO::getStatusCode, reqVO.getStatusCode());
|
|
||||||
queryWrapper.eqIfPresent(ProjectTaskDO::getPriority, reqVO.getPriority());
|
|
||||||
queryWrapper.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime());
|
|
||||||
queryWrapper.orderByAsc(ProjectTaskDO::getParentTaskId);
|
|
||||||
queryWrapper.orderByAsc(ProjectTaskDO::getPriority);
|
|
||||||
queryWrapper.orderByDesc(BaseDO::getCreateTime);
|
|
||||||
queryWrapper.orderByDesc(ProjectTaskDO::getId);
|
|
||||||
if (StringUtils.hasText(reqVO.getKeyword())) {
|
|
||||||
queryWrapper.and(wrapper -> wrapper.like(ProjectTaskDO::getTaskTitle, reqVO.getKeyword()));
|
|
||||||
}
|
|
||||||
if (!scope.seesAll()) {
|
|
||||||
queryWrapper.in(ProjectTaskDO::getId, scope.taskIds());
|
|
||||||
}
|
|
||||||
return selectPage(reqVO, queryWrapper);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行内任务分页查询。
|
||||||
|
* <p>SQL 用 @Select 显式表别名 t,EXISTS 子查询用 t.id 关联 rdms_task_assignee;
|
||||||
|
* 与项目级 aggregate page 同款风格。
|
||||||
|
*
|
||||||
|
* <p>involveUserId 非空时附加 (owner_id = involveUserId OR 活跃协办含 involveUserId);
|
||||||
|
* 与 ownerId 文档标注「二选一」由前端保证(不做后端互斥校验)。
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT t.*
|
||||||
|
FROM rdms_task t
|
||||||
|
<where>
|
||||||
|
t.deleted = b'0'
|
||||||
|
AND t.project_id = #{projectId}
|
||||||
|
AND t.execution_id = #{executionId}
|
||||||
|
<if test="reqVO.parentTaskId != null">
|
||||||
|
AND t.parent_task_id = #{reqVO.parentTaskId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.ownerId != null">
|
||||||
|
AND t.owner_id = #{reqVO.ownerId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.statusCode != null and reqVO.statusCode != ''">
|
||||||
|
AND t.status_code = #{reqVO.statusCode}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.priority != null and reqVO.priority != ''">
|
||||||
|
AND t.priority = #{reqVO.priority}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.updateTime != null and reqVO.updateTime.length == 2">
|
||||||
|
AND t.update_time BETWEEN #{reqVO.updateTime[0]} AND #{reqVO.updateTime[1]}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.keyword != null and reqVO.keyword != ''">
|
||||||
|
AND t.task_title LIKE CONCAT('%', #{reqVO.keyword}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.involveUserId != null">
|
||||||
|
AND (
|
||||||
|
t.owner_id = #{reqVO.involveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_task_assignee a
|
||||||
|
WHERE a.task_id = t.id
|
||||||
|
AND a.user_id = #{reqVO.involveUserId}
|
||||||
|
AND a.removed_at IS NULL
|
||||||
|
AND a.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY t.parent_task_id ASC, t.priority ASC, t.create_time DESC, t.id DESC
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
IPage<ProjectTaskDO> doSelectPageByExecutionId(
|
||||||
|
@Param("projectId") Long projectId,
|
||||||
|
@Param("executionId") Long executionId,
|
||||||
|
@Param("reqVO") ProjectTaskPageReqVO reqVO,
|
||||||
|
Page<ProjectTaskDO> page);
|
||||||
|
|
||||||
default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) {
|
default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) {
|
||||||
ProjectTaskDO update = new ProjectTaskDO();
|
ProjectTaskDO update = new ProjectTaskDO();
|
||||||
update.setStatusCode(toStatus);
|
update.setStatusCode(toStatus);
|
||||||
@@ -186,6 +223,26 @@ public interface ProjectTaskMapper extends BaseMapperX<ProjectTaskDO> {
|
|||||||
@Param("executionIds") Collection<Long> executionIds,
|
@Param("executionIds") Collection<Long> executionIds,
|
||||||
@Param("excludedStatusCodes") Collection<String> excludedStatusCodes);
|
@Param("excludedStatusCodes") Collection<String> excludedStatusCodes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目进度推算:跨执行聚合,按项目下所有根任务 progressRate 简单平均;无根任务时 SQL 返回 null。
|
||||||
|
* 与执行口径一致(parent_task_id IS NULL + excludedStatusCodes),区别仅是不限定 execution_id。
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT AVG(COALESCE(progress_rate, 0))
|
||||||
|
FROM rdms_task
|
||||||
|
WHERE deleted = b'0'
|
||||||
|
AND project_id = #{projectId}
|
||||||
|
AND parent_task_id IS NULL
|
||||||
|
<if test="excludedStatusCodes != null and excludedStatusCodes.size() > 0">
|
||||||
|
AND status_code NOT IN
|
||||||
|
<foreach collection="excludedStatusCodes" item="statusCode" open="(" separator="," close=")">#{statusCode}</foreach>
|
||||||
|
</if>
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
BigDecimal selectRootTaskAvgProgressByProjectId(@Param("projectId") Long projectId,
|
||||||
|
@Param("excludedStatusCodes") Collection<String> excludedStatusCodes);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行详情完成态聚合:返回根任务总数与"已完成"数,用于 execution complete 按钮判定。
|
* 执行详情完成态聚合:返回根任务总数与"已完成"数,用于 execution complete 按钮判定。
|
||||||
* 与 selectRootTaskAvgProgressByExecutionId 共用同一根任务筛选口径(execution_id + parent_task_id IS NULL + excludedStatusCodes)。
|
* 与 selectRootTaskAvgProgressByExecutionId 共用同一根任务筛选口径(execution_id + parent_task_id IS NULL + excludedStatusCodes)。
|
||||||
@@ -249,78 +306,57 @@ public interface ProjectTaskMapper extends BaseMapperX<ProjectTaskDO> {
|
|||||||
.eq(ProjectTaskDO::getId, id));
|
.eq(ProjectTaskDO::getId, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 递归 CTE:从"userId 在指定项目下作为 owner_id 的全部任务"出发,向下展开包含所有子孙的任务 ID。
|
|
||||||
* 用于 VisibilityScopeResolver 中"任务负责人 → 自己 + 全部子孙"规则的 scope 收集。
|
|
||||||
*
|
|
||||||
* 任务表已逻辑删除的行不参与递归(WHERE 子句过滤 deleted)。
|
|
||||||
* 单棵子树最大深度受 MySQL `cte_max_recursion_depth`(默认 1000)限制,业务实际任务树远低于此。
|
|
||||||
*/
|
|
||||||
@Select("""
|
|
||||||
WITH RECURSIVE owned (id) AS (
|
|
||||||
SELECT id FROM rdms_task
|
|
||||||
WHERE deleted = b'0'
|
|
||||||
AND project_id = #{projectId}
|
|
||||||
AND owner_id = #{userId}
|
|
||||||
UNION ALL
|
|
||||||
SELECT t.id FROM rdms_task t
|
|
||||||
JOIN owned o ON t.parent_task_id = o.id
|
|
||||||
WHERE t.deleted = b'0'
|
|
||||||
)
|
|
||||||
SELECT id FROM owned
|
|
||||||
""")
|
|
||||||
List<Long> selectOwnedTaskAndDescendantIdsByProjectIdAndUserId(
|
|
||||||
@Param("projectId") Long projectId,
|
|
||||||
@Param("userId") Long userId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 同 selectOwnedTaskAndDescendantIdsByProjectIdAndUserId 但再加 execution_id 维度。
|
|
||||||
* 注意:递归向下展开只跟着 parent_task_id,子任务必然与父任务在同一 execution 下,
|
|
||||||
* 因此 execution_id 过滤仅作用于种子(owned)那一步即可。
|
|
||||||
*/
|
|
||||||
@Select("""
|
|
||||||
WITH RECURSIVE owned (id) AS (
|
|
||||||
SELECT id FROM rdms_task
|
|
||||||
WHERE deleted = b'0'
|
|
||||||
AND project_id = #{projectId}
|
|
||||||
AND execution_id = #{executionId}
|
|
||||||
AND owner_id = #{userId}
|
|
||||||
UNION ALL
|
|
||||||
SELECT t.id FROM rdms_task t
|
|
||||||
JOIN owned o ON t.parent_task_id = o.id
|
|
||||||
WHERE t.deleted = b'0'
|
|
||||||
)
|
|
||||||
SELECT id FROM owned
|
|
||||||
""")
|
|
||||||
List<Long> selectOwnedTaskAndDescendantIdsByExecutionIdAndUserId(
|
|
||||||
@Param("projectId") Long projectId,
|
|
||||||
@Param("executionId") Long executionId,
|
|
||||||
@Param("userId") Long userId);
|
|
||||||
|
|
||||||
default Integer countByProjectIdAndExecutionIdAndStatusCode(Long projectId, Long executionId,
|
default Integer countByProjectIdAndExecutionIdAndStatusCode(Long projectId, Long executionId,
|
||||||
VisibilityScope scope,
|
|
||||||
ProjectTaskStatusBoardReqVO reqVO,
|
ProjectTaskStatusBoardReqVO reqVO,
|
||||||
String statusCode) {
|
String statusCode) {
|
||||||
// 可见性短路:非 seesAll 且无任何可见任务 → 0
|
return Math.toIntExact(doCountByProjectIdAndExecutionIdAndStatusCode(projectId, executionId, reqVO, statusCode));
|
||||||
if (!scope.seesAll() && scope.taskIds().isEmpty()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
LambdaQueryWrapperX<ProjectTaskDO> queryWrapper = new LambdaQueryWrapperX<ProjectTaskDO>()
|
|
||||||
.eq(ProjectTaskDO::getProjectId, projectId)
|
|
||||||
.eq(ProjectTaskDO::getExecutionId, executionId)
|
|
||||||
.eq(ProjectTaskDO::getStatusCode, statusCode)
|
|
||||||
.eqIfPresent(ProjectTaskDO::getParentTaskId, reqVO.getParentTaskId())
|
|
||||||
.eqIfPresent(ProjectTaskDO::getOwnerId, reqVO.getOwnerId())
|
|
||||||
.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime());
|
|
||||||
if (StringUtils.hasText(reqVO.getKeyword())) {
|
|
||||||
queryWrapper.and(wrapper -> wrapper.like(ProjectTaskDO::getTaskTitle, reqVO.getKeyword()));
|
|
||||||
}
|
|
||||||
if (!scope.seesAll()) {
|
|
||||||
queryWrapper.in(ProjectTaskDO::getId, scope.taskIds());
|
|
||||||
}
|
|
||||||
return Math.toIntExact(selectCount(queryWrapper));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行内任务按状态计数(与 doSelectPageByExecutionId 同款过滤口径,含 involveUserId 协办分支)。
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM rdms_task t
|
||||||
|
<where>
|
||||||
|
t.deleted = b'0'
|
||||||
|
AND t.project_id = #{projectId}
|
||||||
|
AND t.execution_id = #{executionId}
|
||||||
|
AND t.status_code = #{statusCode}
|
||||||
|
<if test="reqVO.parentTaskId != null">
|
||||||
|
AND t.parent_task_id = #{reqVO.parentTaskId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.ownerId != null">
|
||||||
|
AND t.owner_id = #{reqVO.ownerId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.updateTime != null and reqVO.updateTime.length == 2">
|
||||||
|
AND t.update_time BETWEEN #{reqVO.updateTime[0]} AND #{reqVO.updateTime[1]}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.keyword != null and reqVO.keyword != ''">
|
||||||
|
AND t.task_title LIKE CONCAT('%', #{reqVO.keyword}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.involveUserId != null">
|
||||||
|
AND (
|
||||||
|
t.owner_id = #{reqVO.involveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_task_assignee a
|
||||||
|
WHERE a.task_id = t.id
|
||||||
|
AND a.user_id = #{reqVO.involveUserId}
|
||||||
|
AND a.removed_at IS NULL
|
||||||
|
AND a.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
Long doCountByProjectIdAndExecutionIdAndStatusCode(
|
||||||
|
@Param("projectId") Long projectId,
|
||||||
|
@Param("executionId") Long executionId,
|
||||||
|
@Param("reqVO") ProjectTaskStatusBoardReqVO reqVO,
|
||||||
|
@Param("statusCode") String statusCode);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 收集执行下的所有任务 id(含子孙——子孙必然同 execution_id,所以一把抓即可)。
|
* 收集执行下的所有任务 id(含子孙——子孙必然同 execution_id,所以一把抓即可)。
|
||||||
* 用于"删除执行"时的级联软删。
|
* 用于"删除执行"时的级联软删。
|
||||||
@@ -339,7 +375,7 @@ public interface ProjectTaskMapper extends BaseMapperX<ProjectTaskDO> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 从给定任务出发,递归向下收集自身 + 所有子孙任务 id(递归 CTE)。
|
* 从给定任务出发,递归向下收集自身 + 所有子孙任务 id(递归 CTE)。
|
||||||
* 用于"删除任务"时的级联软删。复用与 selectOwnedTaskAndDescendantIdsByProjectIdAndUserId 同款 CTE 模式。
|
* 用于"删除任务"时的级联软删。
|
||||||
*
|
*
|
||||||
* 任务表已逻辑删除的行不参与递归。
|
* 任务表已逻辑删除的行不参与递归。
|
||||||
*/
|
*/
|
||||||
@@ -379,4 +415,281 @@ public interface ProjectTaskMapper extends BaseMapperX<ProjectTaskDO> {
|
|||||||
.eq(ProjectTaskDO::getExecutionId, executionId)));
|
.eq(ProjectTaskDO::getExecutionId, executionId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================== 项目级跨执行聚合查询 ========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目级跨执行任务分页查询。
|
||||||
|
*
|
||||||
|
* 语义:
|
||||||
|
* - involveUserId 不为 null → 附加 (t.owner_id = ? OR exists active assignee user_id = ?)
|
||||||
|
* - statusCodes 非空 → t.status_code IN (...)
|
||||||
|
* - dueRange='overdue' 且 terminalStatusCodes 非空 → 排除终态
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT t.*
|
||||||
|
FROM rdms_task t
|
||||||
|
<where>
|
||||||
|
t.project_id = #{projectId}
|
||||||
|
AND t.deleted = b'0'
|
||||||
|
<if test="reqVO.keyword != null and reqVO.keyword != ''">
|
||||||
|
AND t.task_title LIKE CONCAT('%', #{reqVO.keyword}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.executionIds != null and !reqVO.executionIds.isEmpty()">
|
||||||
|
AND t.execution_id IN
|
||||||
|
<foreach collection="reqVO.executionIds" item="eid" open="(" separator="," close=")">#{eid}</foreach>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.executionStatusCodes != null and !reqVO.executionStatusCodes.isEmpty()">
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM rdms_project_execution e
|
||||||
|
WHERE e.id = t.execution_id
|
||||||
|
AND e.deleted = b'0'
|
||||||
|
AND e.status_code IN
|
||||||
|
<foreach collection="reqVO.executionStatusCodes" item="esc" open="(" separator="," close=")">#{esc}</foreach>
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.executionInvolveUserId != null">
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM rdms_project_execution e
|
||||||
|
WHERE e.id = t.execution_id
|
||||||
|
AND e.deleted = b'0'
|
||||||
|
AND (
|
||||||
|
e.owner_id = #{reqVO.executionInvolveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_execution_assignee ea
|
||||||
|
WHERE ea.execution_id = e.id
|
||||||
|
AND ea.user_id = #{reqVO.executionInvolveUserId}
|
||||||
|
AND ea.removed_at IS NULL
|
||||||
|
AND ea.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.involveUserId != null">
|
||||||
|
AND (
|
||||||
|
t.owner_id = #{reqVO.involveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_task_assignee a
|
||||||
|
WHERE a.task_id = t.id
|
||||||
|
AND a.user_id = #{reqVO.involveUserId}
|
||||||
|
AND a.removed_at IS NULL
|
||||||
|
AND a.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.ownerId != null">
|
||||||
|
AND t.owner_id = #{reqVO.ownerId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.statusCodes != null and !reqVO.statusCodes.isEmpty()">
|
||||||
|
AND t.status_code IN
|
||||||
|
<foreach collection="reqVO.statusCodes" item="sc" open="(" separator="," close=")">#{sc}</foreach>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.priority != null and reqVO.priority != ''">
|
||||||
|
AND t.priority = #{reqVO.priority}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.parentTaskId != null">
|
||||||
|
AND t.parent_task_id = #{reqVO.parentTaskId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'overdue'">
|
||||||
|
AND t.planned_end_date < #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'today'">
|
||||||
|
AND t.planned_end_date = #{today}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'thisWeek'">
|
||||||
|
AND t.planned_end_date BETWEEN #{weekStart} AND #{weekEnd}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.updateTime != null and reqVO.updateTime.length == 2">
|
||||||
|
AND t.update_time BETWEEN #{reqVO.updateTime[0]} AND #{reqVO.updateTime[1]}
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
<choose>
|
||||||
|
<when test="reqVO.sortBy == 'priority'">ORDER BY t.priority</when>
|
||||||
|
<when test="reqVO.sortBy == 'updateTime'">ORDER BY t.update_time</when>
|
||||||
|
<when test="reqVO.sortBy == 'createTime'">ORDER BY t.create_time</when>
|
||||||
|
<otherwise>ORDER BY t.planned_end_date</otherwise>
|
||||||
|
</choose>
|
||||||
|
<choose>
|
||||||
|
<when test="reqVO.sortOrder == 'desc'">DESC</when>
|
||||||
|
<otherwise>ASC</otherwise>
|
||||||
|
</choose>
|
||||||
|
, t.id DESC
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
IPage<ProjectTaskDO> selectAggregatePageByProjectId(
|
||||||
|
@Param("projectId") Long projectId,
|
||||||
|
@Param("reqVO") ProjectTaskAggregatePageReqVO reqVO,
|
||||||
|
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes,
|
||||||
|
@Param("weekStart") LocalDate weekStart,
|
||||||
|
@Param("weekEnd") LocalDate weekEnd,
|
||||||
|
@Param("today") LocalDate today,
|
||||||
|
Page<ProjectTaskDO> page);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目级跨执行任务按状态分组计数(status-board)。
|
||||||
|
* 入参同 page 但去除分页 / sort / statusCodes 筛选。
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT t.status_code AS statusCode, COUNT(*) AS count
|
||||||
|
FROM rdms_task t
|
||||||
|
<where>
|
||||||
|
t.project_id = #{projectId}
|
||||||
|
AND t.deleted = b'0'
|
||||||
|
<if test="reqVO.keyword != null and reqVO.keyword != ''">
|
||||||
|
AND t.task_title LIKE CONCAT('%', #{reqVO.keyword}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.executionIds != null and !reqVO.executionIds.isEmpty()">
|
||||||
|
AND t.execution_id IN
|
||||||
|
<foreach collection="reqVO.executionIds" item="eid" open="(" separator="," close=")">#{eid}</foreach>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.executionStatusCodes != null and !reqVO.executionStatusCodes.isEmpty()">
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM rdms_project_execution e
|
||||||
|
WHERE e.id = t.execution_id
|
||||||
|
AND e.deleted = b'0'
|
||||||
|
AND e.status_code IN
|
||||||
|
<foreach collection="reqVO.executionStatusCodes" item="esc" open="(" separator="," close=")">#{esc}</foreach>
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.executionInvolveUserId != null">
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM rdms_project_execution e
|
||||||
|
WHERE e.id = t.execution_id
|
||||||
|
AND e.deleted = b'0'
|
||||||
|
AND (
|
||||||
|
e.owner_id = #{reqVO.executionInvolveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_execution_assignee ea
|
||||||
|
WHERE ea.execution_id = e.id
|
||||||
|
AND ea.user_id = #{reqVO.executionInvolveUserId}
|
||||||
|
AND ea.removed_at IS NULL
|
||||||
|
AND ea.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.involveUserId != null">
|
||||||
|
AND (
|
||||||
|
t.owner_id = #{reqVO.involveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_task_assignee a
|
||||||
|
WHERE a.task_id = t.id
|
||||||
|
AND a.user_id = #{reqVO.involveUserId}
|
||||||
|
AND a.removed_at IS NULL
|
||||||
|
AND a.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.ownerId != null">
|
||||||
|
AND t.owner_id = #{reqVO.ownerId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.priority != null and reqVO.priority != ''">
|
||||||
|
AND t.priority = #{reqVO.priority}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.parentTaskId != null">
|
||||||
|
AND t.parent_task_id = #{reqVO.parentTaskId}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'overdue'">
|
||||||
|
AND t.planned_end_date < #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'today'">
|
||||||
|
AND t.planned_end_date = #{today}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.dueRange == 'thisWeek'">
|
||||||
|
AND t.planned_end_date BETWEEN #{weekStart} AND #{weekEnd}
|
||||||
|
</if>
|
||||||
|
<if test="reqVO.updateTime != null and reqVO.updateTime.length == 2">
|
||||||
|
AND t.update_time BETWEEN #{reqVO.updateTime[0]} AND #{reqVO.updateTime[1]}
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
GROUP BY t.status_code
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
List<StatusCountRow> selectAggregateStatusCount(
|
||||||
|
@Param("projectId") Long projectId,
|
||||||
|
@Param("reqVO") ProjectTaskAggregateStatusBoardReqVO reqVO,
|
||||||
|
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes,
|
||||||
|
@Param("weekStart") LocalDate weekStart,
|
||||||
|
@Param("weekEnd") LocalDate weekEnd,
|
||||||
|
@Param("today") LocalDate today);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* summary 的 4 个数字一次查出来,避免 4 次扫表。
|
||||||
|
*
|
||||||
|
* 返回 Map 结构:
|
||||||
|
* overdue → Long
|
||||||
|
* dueToday → Long
|
||||||
|
* dueThisWeek → Long
|
||||||
|
* doneThisWeek → Long
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT
|
||||||
|
CAST(SUM(CASE WHEN t.planned_end_date < #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
THEN 1 ELSE 0 END) AS SIGNED) AS overdue,
|
||||||
|
CAST(SUM(CASE WHEN t.planned_end_date = #{today}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
THEN 1 ELSE 0 END) AS SIGNED) AS dueToday,
|
||||||
|
CAST(SUM(CASE WHEN t.planned_end_date BETWEEN #{weekStart} AND #{weekEnd}
|
||||||
|
<if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
|
||||||
|
AND t.status_code NOT IN
|
||||||
|
<foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
|
||||||
|
</if>
|
||||||
|
THEN 1 ELSE 0 END) AS SIGNED) AS dueThisWeek,
|
||||||
|
CAST(SUM(CASE WHEN t.actual_end_date BETWEEN #{weekStart} AND #{today}
|
||||||
|
AND t.status_code = #{completedStatusCode}
|
||||||
|
THEN 1 ELSE 0 END) AS SIGNED) AS doneThisWeek
|
||||||
|
FROM rdms_task t
|
||||||
|
<where>
|
||||||
|
t.project_id = #{projectId}
|
||||||
|
AND t.deleted = b'0'
|
||||||
|
<if test="involveUserId != null">
|
||||||
|
AND (
|
||||||
|
t.owner_id = #{involveUserId}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM rdms_task_assignee a
|
||||||
|
WHERE a.task_id = t.id
|
||||||
|
AND a.user_id = #{involveUserId}
|
||||||
|
AND a.removed_at IS NULL
|
||||||
|
AND a.deleted = b'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
Map<String, Long> selectAggregateSummaryCounts(
|
||||||
|
@Param("projectId") Long projectId,
|
||||||
|
@Param("involveUserId") Long involveUserId,
|
||||||
|
@Param("terminalStatusCodes") Collection<String> terminalStatusCodes,
|
||||||
|
@Param("completedStatusCode") String completedStatusCode,
|
||||||
|
@Param("today") LocalDate today,
|
||||||
|
@Param("weekStart") LocalDate weekStart,
|
||||||
|
@Param("weekEnd") LocalDate weekEnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* status-board 计数行 — 内嵌静态类,与 mapper 共享生命周期。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
class StatusCountRow {
|
||||||
|
private String statusCode;
|
||||||
|
private Long count;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
|||||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskAssigneeDO;
|
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskAssigneeDO;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
import org.apache.ibatis.annotations.Select;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -50,41 +48,6 @@ public interface TaskAssigneeMapper extends BaseMapperX<TaskAssigneeDO> {
|
|||||||
.orderByAsc(TaskAssigneeDO::getId));
|
.orderByAsc(TaskAssigneeDO::getId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 查 userId 在指定项目下,当前活跃协办的所有任务 ID(removed_at IS NULL)。
|
|
||||||
* 走 JOIN 是因为 task_assignee 表没有 project_id 冗余字段。
|
|
||||||
* 用于 VisibilityScopeResolver 收集"我是任务协办人"的 scope 来源(项目维度)。
|
|
||||||
*/
|
|
||||||
@Select("""
|
|
||||||
SELECT a.task_id
|
|
||||||
FROM rdms_task_assignee a
|
|
||||||
JOIN rdms_task t ON t.id = a.task_id AND t.deleted = b'0'
|
|
||||||
WHERE a.deleted = b'0'
|
|
||||||
AND a.removed_at IS NULL
|
|
||||||
AND t.project_id = #{projectId}
|
|
||||||
AND a.user_id = #{userId}
|
|
||||||
""")
|
|
||||||
List<Long> selectActiveTaskIdsByProjectIdAndUserId(@Param("projectId") Long projectId,
|
|
||||||
@Param("userId") Long userId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 同上,但再加 execution_id 维度,用于"任务分页(执行内)"的 scope。
|
|
||||||
*/
|
|
||||||
@Select("""
|
|
||||||
SELECT a.task_id
|
|
||||||
FROM rdms_task_assignee a
|
|
||||||
JOIN rdms_task t ON t.id = a.task_id AND t.deleted = b'0'
|
|
||||||
WHERE a.deleted = b'0'
|
|
||||||
AND a.removed_at IS NULL
|
|
||||||
AND t.project_id = #{projectId}
|
|
||||||
AND t.execution_id = #{executionId}
|
|
||||||
AND a.user_id = #{userId}
|
|
||||||
""")
|
|
||||||
List<Long> selectActiveTaskIdsByProjectIdAndExecutionIdAndUserId(
|
|
||||||
@Param("projectId") Long projectId,
|
|
||||||
@Param("executionId") Long executionId,
|
|
||||||
@Param("userId") Long userId);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按主键 + 任务 ID 双键查;返回的记录可能已失效(removed_at != null),由调用方判断。
|
* 按主键 + 任务 ID 双键查;返回的记录可能已失效(removed_at != null),由调用方判断。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.njcn.rdms.module.project.enums;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务到期范围快速筛选枚举(用于跨执行任务查询 chip)。
|
||||||
|
*
|
||||||
|
* <p>含义:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #OVERDUE} 计划完成日 < 今天,且任务状态非终态</li>
|
||||||
|
* <li>{@link #TODAY} 计划完成日 = 今天</li>
|
||||||
|
* <li>{@link #THIS_WEEK} 计划完成日 在本周(周一~周日,Asia/Shanghai)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>非终态的判定走 {@code ObjectStatusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled('task')}
|
||||||
|
* 动态查询,不在此枚举里硬编码状态字面值。</p>
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum DueRangeEnum {
|
||||||
|
|
||||||
|
OVERDUE("overdue"),
|
||||||
|
TODAY("today"),
|
||||||
|
THIS_WEEK("thisWeek");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
public static DueRangeEnum of(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
for (DueRangeEnum e : values()) {
|
||||||
|
if (e.value.equals(value)) return e;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,4 +19,16 @@ public interface ObjectPermissionService {
|
|||||||
*/
|
*/
|
||||||
void checkPermission(Long objectId, String permission, boolean memberOnly);
|
void checkPermission(Long objectId, String permission, boolean memberOnly);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前登录用户是否具备指定对象上的指定权限码(非抛模式)。
|
||||||
|
*
|
||||||
|
* 与 {@link #checkPermission(Long, String, boolean)} 区别:
|
||||||
|
* 本方法不抛异常,纯返回 boolean,用于"无权限就走降级路径"而非"无权限就 403"的场景。
|
||||||
|
*
|
||||||
|
* @param objectId 对象 ID(如 projectId)
|
||||||
|
* @param permission 权限码,如 {@code project:task:query}
|
||||||
|
* @return true=具备,false=不具备
|
||||||
|
*/
|
||||||
|
boolean hasPermission(Long objectId, String permission);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
|||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil
|
|||||||
/**
|
/**
|
||||||
* 产品对象权限服务。
|
* 产品对象权限服务。
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class ProductObjectPermissionService implements ObjectPermissionService {
|
public class ProductObjectPermissionService implements ObjectPermissionService {
|
||||||
|
|
||||||
@@ -36,6 +38,15 @@ public class ProductObjectPermissionService implements ObjectPermissionService {
|
|||||||
return ProductObjectConstants.OBJECT_TYPE;
|
return ProductObjectConstants.OBJECT_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(Long objectId, String permission) {
|
||||||
|
// 当前产品域无 hasPermission 非抛模式调用场景,预留空实现。
|
||||||
|
// 启用时参考 ProjectObjectPermissionService.hasPermission 同款实现。
|
||||||
|
log.warn("[ProductObjectPermissionService.hasPermission] 未实现,默认返回 false;objectId={}, permission={}",
|
||||||
|
objectId, permission);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void checkPermission(Long objectId, String permission, boolean memberOnly) {
|
public void checkPermission(Long objectId, String permission, boolean memberOnly) {
|
||||||
if (objectId == null) {
|
if (objectId == null) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.njcn.rdms.module.project.framework.security.service;
|
package com.njcn.rdms.module.project.framework.security.service;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||||
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
|
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
|
||||||
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
||||||
@@ -8,6 +9,7 @@ import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
|||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
@@ -23,6 +25,7 @@ import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil
|
|||||||
/**
|
/**
|
||||||
* 项目对象权限服务。
|
* 项目对象权限服务。
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class ProjectObjectPermissionService implements ObjectPermissionService {
|
public class ProjectObjectPermissionService implements ObjectPermissionService {
|
||||||
|
|
||||||
@@ -53,7 +56,7 @@ public class ProjectObjectPermissionService implements ObjectPermissionService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String normalizedPermission = normalizePermission(permission);
|
String normalizedPermission = normalizePermission(permission);
|
||||||
// 任一角色含该权限码即放行(等价于多角色 union;短路求值,权限码命中早 return)
|
// 任一角色含该权限码即放行(等价于多角色 union;短路求值)
|
||||||
boolean allowed = userRoles.stream()
|
boolean allowed = userRoles.stream()
|
||||||
.map(UserObjectRoleDO::getRoleId)
|
.map(UserObjectRoleDO::getRoleId)
|
||||||
.distinct()
|
.distinct()
|
||||||
@@ -63,6 +66,33 @@ public class ProjectObjectPermissionService implements ObjectPermissionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(Long objectId, String permission) {
|
||||||
|
if (objectId == null || StrUtil.isBlank(permission)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 先确认是对象成员,非成员直接返回 false
|
||||||
|
List<UserObjectRoleDO> userRoles = userObjectRoleMapper
|
||||||
|
.selectActiveListByObjectAndUserId(ProjectObjectConstants.OBJECT_TYPE, objectId, userId);
|
||||||
|
if (userRoles.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String normalizedPermission = permission.trim();
|
||||||
|
return userRoles.stream()
|
||||||
|
.map(UserObjectRoleDO::getRoleId)
|
||||||
|
.distinct()
|
||||||
|
.anyMatch(roleId -> getRolePermissions(roleId).contains(normalizedPermission));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[hasPermission] objectId={}, permission={} 查询权限失败", objectId, permission, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Set<String> getRolePermissions(Long roleId) {
|
private Set<String> getRolePermissions(Long roleId) {
|
||||||
Set<String> permissions = objectPermissionApi
|
Set<String> permissions = objectPermissionApi
|
||||||
.getObjectRolePermissions(roleId, ObjectRoleConstants.ROLE_SCOPE_OBJECT, ProjectObjectConstants.OBJECT_TYPE)
|
.getObjectRolePermissions(roleId, ObjectRoleConstants.ROLE_SCOPE_OBJECT, ProjectObjectConstants.OBJECT_TYPE)
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.njcn.rdms.module.project.service.overtime;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationExportVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationSaveReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusActionReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusLogRespVO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface OvertimeApplicationService {
|
||||||
|
|
||||||
|
Long createApplication(OvertimeApplicationSaveReqVO reqVO);
|
||||||
|
|
||||||
|
void updateRejectedApplication(Long id, OvertimeApplicationSaveReqVO reqVO);
|
||||||
|
|
||||||
|
void approve(Long id, OvertimeApplicationStatusActionReqVO reqVO);
|
||||||
|
|
||||||
|
void reject(Long id, OvertimeApplicationStatusActionReqVO reqVO);
|
||||||
|
|
||||||
|
void cancel(Long id, OvertimeApplicationStatusActionReqVO reqVO);
|
||||||
|
|
||||||
|
void deleteApplication(Long id);
|
||||||
|
|
||||||
|
OvertimeApplicationRespVO getApplication(Long id);
|
||||||
|
|
||||||
|
PageResult<OvertimeApplicationRespVO> getMyPage(OvertimeApplicationPageReqVO reqVO);
|
||||||
|
|
||||||
|
PageResult<OvertimeApplicationRespVO> getApprovalPage(OvertimeApplicationPageReqVO reqVO);
|
||||||
|
|
||||||
|
List<OvertimeApplicationStatusLogRespVO> getStatusLogs(Long id);
|
||||||
|
|
||||||
|
List<OvertimeApplicationExportVO> getExportList(OvertimeApplicationPageReqVO reqVO);
|
||||||
|
}
|
||||||
@@ -0,0 +1,467 @@
|
|||||||
|
package com.njcn.rdms.module.project.service.overtime;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
|
import com.njcn.rdms.framework.common.util.json.JsonUtils;
|
||||||
|
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||||
|
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||||
|
import com.njcn.rdms.module.project.constant.OvertimeApplicationConstants;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationExportVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationSaveReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusActionReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusLogRespVO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationDO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationStatusLogDO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.overtime.OvertimeApplicationMapper;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.overtime.OvertimeApplicationStatusLogMapper;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
|
||||||
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
|
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||||
|
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||||
|
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class OvertimeApplicationServiceImpl implements OvertimeApplicationService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private OvertimeApplicationMapper overtimeApplicationMapper;
|
||||||
|
@Resource
|
||||||
|
private OvertimeApplicationStatusLogMapper overtimeApplicationStatusLogMapper;
|
||||||
|
@Resource
|
||||||
|
private BizAuditLogMapper bizAuditLogMapper;
|
||||||
|
@Resource
|
||||||
|
private ObjectStatusModelMapper objectStatusModelMapper;
|
||||||
|
@Resource
|
||||||
|
private ObjectStatusTransitionMapper objectStatusTransitionMapper;
|
||||||
|
@Resource
|
||||||
|
private AdminUserApi adminUserApi;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Long createApplication(OvertimeApplicationSaveReqVO reqVO) {
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
AdminUserRespDTO approver = validateApprover(reqVO.getApproverId());
|
||||||
|
String initialStatus = getInitialStatusCode();
|
||||||
|
|
||||||
|
OvertimeApplicationDO application = new OvertimeApplicationDO();
|
||||||
|
application.setApplicantId(loginUserId);
|
||||||
|
application.setApplicantName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||||
|
applySaveFields(application, reqVO, approver);
|
||||||
|
application.setStatusCode(initialStatus);
|
||||||
|
application.setApprovalComment(null);
|
||||||
|
application.setSubmitTime(LocalDateTime.now());
|
||||||
|
application.setApprovalTime(null);
|
||||||
|
overtimeApplicationMapper.insert(application);
|
||||||
|
|
||||||
|
writeStatusLog(application, OvertimeApplicationConstants.ACTION_SUBMIT, null, initialStatus, null);
|
||||||
|
writeAuditLog(application, OvertimeApplicationConstants.ACTION_SUBMIT, null, initialStatus, null, null, null);
|
||||||
|
return application.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void updateRejectedApplication(Long id, OvertimeApplicationSaveReqVO reqVO) {
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
OvertimeApplicationDO current = validateApplicationExists(id);
|
||||||
|
if (!Objects.equals(current.getApplicantId(), loginUserId)) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPLICANT_ONLY);
|
||||||
|
}
|
||||||
|
String fromStatus = current.getStatusCode();
|
||||||
|
ObjectStatusTransitionDO transition = validateTransition(fromStatus, OvertimeApplicationConstants.ACTION_RESUBMIT,
|
||||||
|
null);
|
||||||
|
AdminUserRespDTO approver = validateApprover(reqVO.getApproverId());
|
||||||
|
|
||||||
|
OvertimeApplicationDO before = cloneApplication(current);
|
||||||
|
OvertimeApplicationDO update = new OvertimeApplicationDO();
|
||||||
|
applySaveFields(update, reqVO, approver);
|
||||||
|
update.setStatusCode(transition.getToStatusCode());
|
||||||
|
update.setApprovalComment(null);
|
||||||
|
update.setSubmitTime(LocalDateTime.now());
|
||||||
|
update.setApprovalTime(null);
|
||||||
|
|
||||||
|
int updateCount = overtimeApplicationMapper.updateByIdAndStatusesAndApplicantId(update, id,
|
||||||
|
List.of(OvertimeApplicationConstants.STATUS_PENDING, OvertimeApplicationConstants.STATUS_REJECTED),
|
||||||
|
loginUserId);
|
||||||
|
if (updateCount != 1) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
OvertimeApplicationDO after = mergeUpdated(current, update);
|
||||||
|
writeStatusLog(after, OvertimeApplicationConstants.ACTION_RESUBMIT, fromStatus, transition.getToStatusCode(),
|
||||||
|
null);
|
||||||
|
writeAuditLog(after, OvertimeApplicationConstants.ACTION_RESUBMIT, fromStatus, transition.getToStatusCode(),
|
||||||
|
buildFieldChanges(before, after), null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void approve(Long id, OvertimeApplicationStatusActionReqVO reqVO) {
|
||||||
|
processApprovalAction(id, OvertimeApplicationConstants.ACTION_APPROVE, reqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void reject(Long id, OvertimeApplicationStatusActionReqVO reqVO) {
|
||||||
|
processApprovalAction(id, OvertimeApplicationConstants.ACTION_REJECT, reqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void cancel(Long id, OvertimeApplicationStatusActionReqVO reqVO) {
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
OvertimeApplicationDO current = validateApplicationExists(id);
|
||||||
|
if (!Objects.equals(current.getApplicantId(), loginUserId)) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPLICANT_ONLY);
|
||||||
|
}
|
||||||
|
String reason = normalizeNullableText(reqVO == null ? null : reqVO.getReason());
|
||||||
|
String fromStatus = current.getStatusCode();
|
||||||
|
ObjectStatusTransitionDO transition = validateTransition(fromStatus, OvertimeApplicationConstants.ACTION_CANCEL,
|
||||||
|
reason);
|
||||||
|
|
||||||
|
OvertimeApplicationDO update = new OvertimeApplicationDO();
|
||||||
|
update.setStatusCode(transition.getToStatusCode());
|
||||||
|
update.setApprovalComment(reason);
|
||||||
|
update.setApprovalTime(LocalDateTime.now());
|
||||||
|
int updateCount = overtimeApplicationMapper.updateByIdAndStatusAndApplicantId(update, id, fromStatus,
|
||||||
|
loginUserId);
|
||||||
|
if (updateCount != 1) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
OvertimeApplicationDO after = mergeUpdated(current, update);
|
||||||
|
writeStatusLog(after, OvertimeApplicationConstants.ACTION_CANCEL, fromStatus, transition.getToStatusCode(),
|
||||||
|
reason);
|
||||||
|
writeAuditLog(after, OvertimeApplicationConstants.ACTION_CANCEL, fromStatus, transition.getToStatusCode(),
|
||||||
|
null, reason, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void deleteApplication(Long id) {
|
||||||
|
OvertimeApplicationDO current = validateApplicationExists(id);
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
if (!Objects.equals(current.getApplicantId(), loginUserId)) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPLICANT_ONLY);
|
||||||
|
}
|
||||||
|
if (!OvertimeApplicationConstants.STATUS_CANCELLED.equals(current.getStatusCode())) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_DELETE_ONLY_CANCELLED);
|
||||||
|
}
|
||||||
|
overtimeApplicationMapper.deleteById(id);
|
||||||
|
writeAuditLog(current, OvertimeApplicationConstants.ACTION_DELETE, current.getStatusCode(), null, null, null,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OvertimeApplicationRespVO getApplication(Long id) {
|
||||||
|
OvertimeApplicationDO application = validateReadableApplication(id);
|
||||||
|
return toRespVO(application);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResult<OvertimeApplicationRespVO> getMyPage(OvertimeApplicationPageReqVO reqVO) {
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
PageResult<OvertimeApplicationDO> page = overtimeApplicationMapper.selectMyPage(loginUserId, reqVO);
|
||||||
|
return BeanUtils.toBean(page, OvertimeApplicationRespVO.class, this::applyStatusView);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResult<OvertimeApplicationRespVO> getApprovalPage(OvertimeApplicationPageReqVO reqVO) {
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
PageResult<OvertimeApplicationDO> page = overtimeApplicationMapper.selectApprovalPage(loginUserId, reqVO);
|
||||||
|
return BeanUtils.toBean(page, OvertimeApplicationRespVO.class, this::applyStatusView);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<OvertimeApplicationStatusLogRespVO> getStatusLogs(Long id) {
|
||||||
|
validateReadableApplication(id);
|
||||||
|
return BeanUtils.toBean(overtimeApplicationStatusLogMapper.selectListByApplicationId(id),
|
||||||
|
OvertimeApplicationStatusLogRespVO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<OvertimeApplicationExportVO> getExportList(OvertimeApplicationPageReqVO reqVO) {
|
||||||
|
reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||||
|
PageResult<OvertimeApplicationRespVO> page = getMyPage(reqVO);
|
||||||
|
return BeanUtils.toBean(page.getList(), OvertimeApplicationExportVO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processApprovalAction(Long id, String actionCode, OvertimeApplicationStatusActionReqVO reqVO) {
|
||||||
|
OvertimeApplicationDO current = validateApplicationExists(id);
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
if (!Objects.equals(current.getApproverId(), loginUserId)) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPROVER_ONLY);
|
||||||
|
}
|
||||||
|
String reason = normalizeNullableText(reqVO == null ? null : reqVO.getReason());
|
||||||
|
String fromStatus = current.getStatusCode();
|
||||||
|
ObjectStatusTransitionDO transition = validateTransition(fromStatus, actionCode, reason);
|
||||||
|
|
||||||
|
OvertimeApplicationDO update = new OvertimeApplicationDO();
|
||||||
|
update.setStatusCode(transition.getToStatusCode());
|
||||||
|
update.setApprovalComment(reason);
|
||||||
|
update.setApprovalTime(LocalDateTime.now());
|
||||||
|
int updateCount = overtimeApplicationMapper.updateByIdAndStatus(update, id, fromStatus);
|
||||||
|
if (updateCount != 1) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
OvertimeApplicationDO after = mergeUpdated(current, update);
|
||||||
|
writeStatusLog(after, actionCode, fromStatus, transition.getToStatusCode(), reason);
|
||||||
|
writeAuditLog(after, actionCode, fromStatus, transition.getToStatusCode(), null, reason, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OvertimeApplicationDO validateApplicationExists(Long id) {
|
||||||
|
OvertimeApplicationDO application = overtimeApplicationMapper.selectById(id);
|
||||||
|
if (application == null) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_NOT_EXISTS);
|
||||||
|
}
|
||||||
|
return application;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OvertimeApplicationDO validateReadableApplication(Long id) {
|
||||||
|
OvertimeApplicationDO application = validateApplicationExists(id);
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
if (!Objects.equals(application.getApplicantId(), loginUserId)
|
||||||
|
&& !Objects.equals(application.getApproverId(), loginUserId)) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_READ_FORBIDDEN);
|
||||||
|
}
|
||||||
|
return application;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectStatusTransitionDO validateTransition(String fromStatus, String actionCode, String reason) {
|
||||||
|
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
||||||
|
.selectByObjectTypeAndFromStatusAndAction(OvertimeApplicationConstants.STATUS_OBJECT_TYPE, fromStatus,
|
||||||
|
actionCode);
|
||||||
|
if (transition == null) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
||||||
|
}
|
||||||
|
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_ACTION_REASON_REQUIRED, actionCode);
|
||||||
|
}
|
||||||
|
ObjectStatusModelDO toModel = objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
|
||||||
|
OvertimeApplicationConstants.STATUS_OBJECT_TYPE, transition.getToStatusCode());
|
||||||
|
if (toModel == null) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
||||||
|
}
|
||||||
|
return transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getInitialStatusCode() {
|
||||||
|
ObjectStatusModelDO statusModel = objectStatusModelMapper
|
||||||
|
.selectInitialByObjectTypeEnabled(OvertimeApplicationConstants.STATUS_OBJECT_TYPE);
|
||||||
|
if (statusModel == null || !StringUtils.hasText(statusModel.getStatusCode())) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
||||||
|
}
|
||||||
|
return statusModel.getStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdminUserRespDTO validateApprover(Long approverId) {
|
||||||
|
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
if (Objects.equals(approverId, loginUserId)) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPROVER_SELF_FORBIDDEN);
|
||||||
|
}
|
||||||
|
return loadUser(approverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdminUserRespDTO loadUser(Long userId) {
|
||||||
|
if (userId == null) {
|
||||||
|
throw invalidParamException("用户编号不能为空");
|
||||||
|
}
|
||||||
|
adminUserApi.validateUserList(Collections.singleton(userId)).getCheckedData();
|
||||||
|
CommonResult<AdminUserRespDTO> result = adminUserApi.getUser(userId);
|
||||||
|
AdminUserRespDTO user = result.getCheckedData();
|
||||||
|
if (user == null) {
|
||||||
|
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPROVER_INVALID);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applySaveFields(OvertimeApplicationDO target, OvertimeApplicationSaveReqVO reqVO,
|
||||||
|
AdminUserRespDTO approver) {
|
||||||
|
target.setOvertimeDate(reqVO.getOvertimeDate());
|
||||||
|
target.setOvertimeDuration(normalizeRequiredText(reqVO.getOvertimeDuration(), "加班时长不能为空"));
|
||||||
|
target.setOvertimeReason(normalizeRequiredText(reqVO.getOvertimeReason(), "加班原因不能为空"));
|
||||||
|
target.setOvertimeContent(normalizeRequiredText(reqVO.getOvertimeContent(), "加班内容不能为空"));
|
||||||
|
target.setApproverId(approver.getId());
|
||||||
|
target.setApproverName(defaultText(approver.getNickname()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private OvertimeApplicationRespVO toRespVO(OvertimeApplicationDO application) {
|
||||||
|
return BeanUtils.toBean(application, OvertimeApplicationRespVO.class, this::applyStatusView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyStatusView(OvertimeApplicationRespVO respVO) {
|
||||||
|
ObjectStatusModelDO statusModel = objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
|
||||||
|
OvertimeApplicationConstants.STATUS_OBJECT_TYPE, respVO.getStatusCode());
|
||||||
|
if (statusModel == null) {
|
||||||
|
respVO.setStatusName(respVO.getStatusCode());
|
||||||
|
respVO.setAllowEdit(false);
|
||||||
|
respVO.setTerminal(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
respVO.setStatusName(statusModel.getStatusName());
|
||||||
|
respVO.setAllowEdit(Boolean.TRUE.equals(statusModel.getAllowEdit()));
|
||||||
|
respVO.setTerminal(Boolean.TRUE.equals(statusModel.getTerminalFlag()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeStatusLog(OvertimeApplicationDO application, String actionType, String fromStatus,
|
||||||
|
String toStatus, String reason) {
|
||||||
|
OvertimeApplicationStatusLogDO log = new OvertimeApplicationStatusLogDO();
|
||||||
|
log.setApplicationId(application.getId());
|
||||||
|
log.setActionType(actionType);
|
||||||
|
log.setFromStatus(fromStatus);
|
||||||
|
log.setToStatus(toStatus);
|
||||||
|
log.setReason(reason);
|
||||||
|
log.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
|
||||||
|
log.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||||
|
log.setApplicantNameSnapshot(application.getApplicantName());
|
||||||
|
log.setOvertimeDateSnapshot(application.getOvertimeDate());
|
||||||
|
log.setOvertimeDurationSnapshot(application.getOvertimeDuration());
|
||||||
|
log.setRemark(buildSnapshotRemark(application));
|
||||||
|
overtimeApplicationStatusLogMapper.insert(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeAuditLog(OvertimeApplicationDO application, String actionType, String fromStatus,
|
||||||
|
String toStatus, String fieldChanges, String reason, String remark) {
|
||||||
|
BizAuditLogDO auditLog = new BizAuditLogDO();
|
||||||
|
auditLog.setBizType(OvertimeApplicationConstants.BIZ_TYPE);
|
||||||
|
auditLog.setBizId(application.getId());
|
||||||
|
auditLog.setActionType(actionType);
|
||||||
|
auditLog.setFromStatus(fromStatus);
|
||||||
|
auditLog.setToStatus(toStatus);
|
||||||
|
auditLog.setFieldChanges(fieldChanges);
|
||||||
|
auditLog.setReason(reason);
|
||||||
|
auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
|
||||||
|
auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||||
|
auditLog.setRemark(StringUtils.hasText(remark) ? remark : buildSnapshotRemark(application));
|
||||||
|
bizAuditLogMapper.insert(auditLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildSnapshotRemark(OvertimeApplicationDO application) {
|
||||||
|
return "申请人:" + defaultText(application.getApplicantName())
|
||||||
|
+ ",加班日期:" + application.getOvertimeDate()
|
||||||
|
+ ",加班时长:" + defaultText(application.getOvertimeDuration());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildFieldChanges(OvertimeApplicationDO before, OvertimeApplicationDO after) {
|
||||||
|
Map<String, Object> fieldChanges = new LinkedHashMap<>();
|
||||||
|
appendFieldChange(fieldChanges, "overtimeDate", valueOf(before, OvertimeApplicationDO::getOvertimeDate),
|
||||||
|
valueOf(after, OvertimeApplicationDO::getOvertimeDate));
|
||||||
|
appendFieldChange(fieldChanges, "overtimeDuration",
|
||||||
|
valueOf(before, OvertimeApplicationDO::getOvertimeDuration),
|
||||||
|
valueOf(after, OvertimeApplicationDO::getOvertimeDuration));
|
||||||
|
appendFieldChange(fieldChanges, "overtimeReason", valueOf(before, OvertimeApplicationDO::getOvertimeReason),
|
||||||
|
valueOf(after, OvertimeApplicationDO::getOvertimeReason));
|
||||||
|
appendFieldChange(fieldChanges, "overtimeContent", valueOf(before, OvertimeApplicationDO::getOvertimeContent),
|
||||||
|
valueOf(after, OvertimeApplicationDO::getOvertimeContent));
|
||||||
|
appendFieldChange(fieldChanges, "approverId", valueOf(before, OvertimeApplicationDO::getApproverId),
|
||||||
|
valueOf(after, OvertimeApplicationDO::getApproverId));
|
||||||
|
appendFieldChange(fieldChanges, "approverName", valueOf(before, OvertimeApplicationDO::getApproverName),
|
||||||
|
valueOf(after, OvertimeApplicationDO::getApproverName));
|
||||||
|
appendFieldChange(fieldChanges, "statusCode", valueOf(before, OvertimeApplicationDO::getStatusCode),
|
||||||
|
valueOf(after, OvertimeApplicationDO::getStatusCode));
|
||||||
|
return fieldChanges.isEmpty() ? null : JsonUtils.toJsonString(fieldChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendFieldChange(Map<String, Object> fieldChanges, String fieldName, Object before, Object after) {
|
||||||
|
if (Objects.equals(before, after)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, Object> value = new LinkedHashMap<>();
|
||||||
|
value.put("before", before);
|
||||||
|
value.put("after", after);
|
||||||
|
fieldChanges.put(fieldName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T valueOf(OvertimeApplicationDO application, Function<OvertimeApplicationDO, T> getter) {
|
||||||
|
return application == null ? null : getter.apply(application);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OvertimeApplicationDO cloneApplication(OvertimeApplicationDO source) {
|
||||||
|
OvertimeApplicationDO target = new OvertimeApplicationDO();
|
||||||
|
target.setId(source.getId());
|
||||||
|
target.setApplicantId(source.getApplicantId());
|
||||||
|
target.setApplicantName(source.getApplicantName());
|
||||||
|
target.setOvertimeDate(source.getOvertimeDate());
|
||||||
|
target.setOvertimeDuration(source.getOvertimeDuration());
|
||||||
|
target.setOvertimeReason(source.getOvertimeReason());
|
||||||
|
target.setOvertimeContent(source.getOvertimeContent());
|
||||||
|
target.setApproverId(source.getApproverId());
|
||||||
|
target.setApproverName(source.getApproverName());
|
||||||
|
target.setStatusCode(source.getStatusCode());
|
||||||
|
target.setApprovalComment(source.getApprovalComment());
|
||||||
|
target.setSubmitTime(source.getSubmitTime());
|
||||||
|
target.setApprovalTime(source.getApprovalTime());
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OvertimeApplicationDO mergeUpdated(OvertimeApplicationDO current, OvertimeApplicationDO update) {
|
||||||
|
OvertimeApplicationDO after = cloneApplication(current);
|
||||||
|
if (update.getOvertimeDate() != null) {
|
||||||
|
after.setOvertimeDate(update.getOvertimeDate());
|
||||||
|
}
|
||||||
|
if (update.getOvertimeDuration() != null) {
|
||||||
|
after.setOvertimeDuration(update.getOvertimeDuration());
|
||||||
|
}
|
||||||
|
if (update.getOvertimeReason() != null) {
|
||||||
|
after.setOvertimeReason(update.getOvertimeReason());
|
||||||
|
}
|
||||||
|
if (update.getOvertimeContent() != null) {
|
||||||
|
after.setOvertimeContent(update.getOvertimeContent());
|
||||||
|
}
|
||||||
|
if (update.getApproverId() != null) {
|
||||||
|
after.setApproverId(update.getApproverId());
|
||||||
|
}
|
||||||
|
if (update.getApproverName() != null) {
|
||||||
|
after.setApproverName(update.getApproverName());
|
||||||
|
}
|
||||||
|
if (update.getStatusCode() != null) {
|
||||||
|
after.setStatusCode(update.getStatusCode());
|
||||||
|
}
|
||||||
|
after.setApprovalComment(update.getApprovalComment());
|
||||||
|
if (update.getSubmitTime() != null) {
|
||||||
|
after.setSubmitTime(update.getSubmitTime());
|
||||||
|
}
|
||||||
|
after.setApprovalTime(update.getApprovalTime());
|
||||||
|
return after;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeRequiredText(String value, String message) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
throw invalidParamException(message);
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeNullableText(String value) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String defaultText(String value) {
|
||||||
|
return StringUtils.hasText(value) ? value : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,4 +60,10 @@ public interface ProjectService {
|
|||||||
|
|
||||||
void autoStartProjectIfPending(Long projectId, String triggerAction);
|
void autoStartProjectIfPending(Long projectId, String triggerAction);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重算项目进度并落库:AVG(项目下所有根任务 progressRate),沿用 task 维度的 progress_excluded 排除集合。
|
||||||
|
* 由任务侧在"任务进度变化 / 状态变更 / 创建删除 / 父子结构变化"等入口触发。
|
||||||
|
*/
|
||||||
|
void recalcProgress(Long projectId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,9 +32,11 @@ import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransition
|
|||||||
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
|
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper;
|
import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper;
|
||||||
|
import com.njcn.rdms.module.project.constant.ProjectTaskConstants;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
|
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementModuleMapper;
|
import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementModuleMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.ProjectStatusLogMapper;
|
import com.njcn.rdms.module.project.dal.mysql.project.ProjectStatusLogMapper;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
|
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
|
||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
@@ -58,6 +60,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -108,6 +111,8 @@ class ProjectServiceImpl implements ProjectService {
|
|||||||
@Resource
|
@Resource
|
||||||
private ProjectRequirementModuleMapper projectRequirementModuleMapper;
|
private ProjectRequirementModuleMapper projectRequirementModuleMapper;
|
||||||
@Resource
|
@Resource
|
||||||
|
private ProjectTaskMapper projectTaskMapper;
|
||||||
|
@Resource
|
||||||
private com.njcn.rdms.module.project.service.member.ObjectRoleAutoAssignService objectRoleAutoAssignService;
|
private com.njcn.rdms.module.project.service.member.ObjectRoleAutoAssignService objectRoleAutoAssignService;
|
||||||
@Resource
|
@Resource
|
||||||
private ObjectDataScopeService objectDataScopeService;
|
private ObjectDataScopeService objectDataScopeService;
|
||||||
@@ -634,6 +639,39 @@ class ProjectServiceImpl implements ProjectService {
|
|||||||
changeStatus(project, transition, ObjectActivityConstants.PROJECT_ACTION_AUTO_START, reason);
|
changeStatus(project, transition, ObjectActivityConstants.PROJECT_ACTION_AUTO_START, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void recalcProgress(Long projectId) {
|
||||||
|
if (projectId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ProjectDO project = projectMapper.selectById(projectId);
|
||||||
|
if (project == null) {
|
||||||
|
// 项目已被删除(删除项目时其下任务已不可达,无须再触发;此处兜底,避免上游误调)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> excludedStatusCodes = objectStatusModelMapper
|
||||||
|
.selectProgressExcludedStatusCodesByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
||||||
|
BigDecimal newProgress = normalizeProgress(projectTaskMapper
|
||||||
|
.selectRootTaskAvgProgressByProjectId(projectId,
|
||||||
|
excludedStatusCodes == null ? Collections.emptyList() : excludedStatusCodes));
|
||||||
|
// 与当前缓存值数值相等则跳过 UPDATE,避免不必要的写与审计字段抖动
|
||||||
|
BigDecimal current = project.getProgressRate();
|
||||||
|
if (current != null && current.compareTo(newProgress) == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
projectMapper.updateProgressRateById(projectId, newProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进度归一化:null → 0.00,非 null → scale=2 HALF_UP。与执行/任务层口径一致。
|
||||||
|
*/
|
||||||
|
private BigDecimal normalizeProgress(BigDecimal progress) {
|
||||||
|
if (progress == null) {
|
||||||
|
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
return progress.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void validateCreateReqVO(ProjectSaveReqVO createReqVO) {
|
void validateCreateReqVO(ProjectSaveReqVO createReqVO) {
|
||||||
validateProjectCodeUnique(null, createReqVO.getProjectCode());
|
validateProjectCodeUnique(null, createReqVO.getProjectCode());
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package com.njcn.rdms.module.project.service.project;
|
package com.njcn.rdms.module.project.service.project;
|
||||||
|
|
||||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
|
||||||
import com.njcn.rdms.module.project.constant.ProjectExecutionConstants;
|
import com.njcn.rdms.module.project.constant.ProjectExecutionConstants;
|
||||||
|
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
||||||
import com.njcn.rdms.module.project.constant.ProjectTaskConstants;
|
import com.njcn.rdms.module.project.constant.ProjectTaskConstants;
|
||||||
|
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusBoardReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusBoardReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusBoardRespVO;
|
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusBoardRespVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskBoardPageReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskBoardPageReqVO;
|
||||||
@@ -12,18 +13,17 @@ import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTask
|
|||||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskRespVO;
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskRespVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardReqVO;
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardReqVO;
|
||||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardRespVO;
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardRespVO;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO;
|
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
|
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
|
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
|
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
|
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
||||||
import com.njcn.rdms.module.project.service.project.permission.VisibilityScope;
|
|
||||||
import com.njcn.rdms.module.project.service.project.permission.VisibilityScopeResolver;
|
|
||||||
import com.njcn.rdms.module.project.service.project.task.ProjectTaskService;
|
import com.njcn.rdms.module.project.service.project.task.ProjectTaskService;
|
||||||
|
import com.njcn.rdms.module.project.util.DueRangeSupport;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
@@ -42,28 +42,28 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService
|
|||||||
@Resource
|
@Resource
|
||||||
private ProjectTaskMapper projectTaskMapper;
|
private ProjectTaskMapper projectTaskMapper;
|
||||||
@Resource
|
@Resource
|
||||||
private VisibilityScopeResolver visibilityScopeResolver;
|
|
||||||
@Resource
|
|
||||||
private ProjectTaskService projectTaskService;
|
private ProjectTaskService projectTaskService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectExecutionConstants.PERMISSION_QUERY)
|
||||||
public ProjectExecutionStatusBoardRespVO getExecutionStatusBoard(Long projectId, ProjectExecutionStatusBoardReqVO reqVO) {
|
public ProjectExecutionStatusBoardRespVO getExecutionStatusBoard(Long projectId, ProjectExecutionStatusBoardReqVO reqVO) {
|
||||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
|
||||||
VisibilityScope scope = visibilityScopeResolver.resolveForProject(projectId, userId);
|
|
||||||
List<ObjectStatusModelDO> statusModels = objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE);
|
List<ObjectStatusModelDO> statusModels = objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE);
|
||||||
return buildExecutionStatusBoard(projectId, scope, reqVO, statusModels);
|
return buildExecutionStatusBoard(projectId, reqVO, statusModels);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectTaskConstants.PERMISSION_QUERY)
|
||||||
public ProjectTaskStatusBoardRespVO getTaskStatusBoard(Long projectId, Long executionId, ProjectTaskStatusBoardReqVO reqVO) {
|
public ProjectTaskStatusBoardRespVO getTaskStatusBoard(Long projectId, Long executionId, ProjectTaskStatusBoardReqVO reqVO) {
|
||||||
VisibilityScope scope = resolveTaskScope(projectId, executionId);
|
|
||||||
List<ObjectStatusModelDO> statusModels = objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
List<ObjectStatusModelDO> statusModels = objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
||||||
return buildTaskStatusBoard(projectId, executionId, scope, reqVO, statusModels);
|
return buildTaskStatusBoard(projectId, executionId, reqVO, statusModels);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectTaskConstants.PERMISSION_QUERY)
|
||||||
public ProjectTaskBoardPageRespVO getTaskBoardPage(Long projectId, Long executionId, ProjectTaskBoardPageReqVO reqVO) {
|
public ProjectTaskBoardPageRespVO getTaskBoardPage(Long projectId, Long executionId, ProjectTaskBoardPageReqVO reqVO) {
|
||||||
VisibilityScope scope = resolveTaskScope(projectId, executionId);
|
|
||||||
List<ObjectStatusModelDO> statusModels = objectStatusModelMapper
|
List<ObjectStatusModelDO> statusModels = objectStatusModelMapper
|
||||||
.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService
|
|||||||
|
|
||||||
ProjectTaskBoardPageRespVO respVO = new ProjectTaskBoardPageRespVO();
|
ProjectTaskBoardPageRespVO respVO = new ProjectTaskBoardPageRespVO();
|
||||||
List<ProjectTaskBoardPageRespVO.ColumnItemVO> items = targetStatusModels.stream()
|
List<ProjectTaskBoardPageRespVO.ColumnItemVO> items = targetStatusModels.stream()
|
||||||
.map(statusModel -> buildBoardColumn(projectId, executionId, scope, reqVO, statusModel))
|
.map(statusModel -> buildBoardColumn(projectId, executionId, reqVO, statusModel))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
respVO.setItems(items);
|
respVO.setItems(items);
|
||||||
return respVO;
|
return respVO;
|
||||||
@@ -98,11 +98,10 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ProjectTaskBoardPageRespVO.ColumnItemVO buildBoardColumn(Long projectId, Long executionId,
|
private ProjectTaskBoardPageRespVO.ColumnItemVO buildBoardColumn(Long projectId, Long executionId,
|
||||||
VisibilityScope scope,
|
|
||||||
ProjectTaskBoardPageReqVO reqVO,
|
ProjectTaskBoardPageReqVO reqVO,
|
||||||
ObjectStatusModelDO statusModel) {
|
ObjectStatusModelDO statusModel) {
|
||||||
ProjectTaskPageReqVO innerReq = toInnerPageReq(reqVO, statusModel.getStatusCode());
|
ProjectTaskPageReqVO innerReq = toInnerPageReq(reqVO, statusModel.getStatusCode());
|
||||||
PageResult<ProjectTaskDO> doPage = projectTaskMapper.selectPageByExecutionId(projectId, executionId, scope, innerReq);
|
PageResult<ProjectTaskDO> doPage = projectTaskMapper.selectPageByExecutionId(projectId, executionId, innerReq);
|
||||||
PageResult<ProjectTaskRespVO> voPage = projectTaskService.assembleTaskRespVOPage(projectId, executionId, doPage);
|
PageResult<ProjectTaskRespVO> voPage = projectTaskService.assembleTaskRespVOPage(projectId, executionId, doPage);
|
||||||
|
|
||||||
ProjectTaskBoardPageRespVO.ColumnItemVO item = new ProjectTaskBoardPageRespVO.ColumnItemVO();
|
ProjectTaskBoardPageRespVO.ColumnItemVO item = new ProjectTaskBoardPageRespVO.ColumnItemVO();
|
||||||
@@ -124,6 +123,7 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService
|
|||||||
innerReq.setPageSize(reqVO.getPageSize());
|
innerReq.setPageSize(reqVO.getPageSize());
|
||||||
innerReq.setKeyword(reqVO.getKeyword());
|
innerReq.setKeyword(reqVO.getKeyword());
|
||||||
innerReq.setParentTaskId(reqVO.getParentTaskId());
|
innerReq.setParentTaskId(reqVO.getParentTaskId());
|
||||||
|
innerReq.setInvolveUserId(reqVO.getInvolveUserId());
|
||||||
innerReq.setOwnerId(reqVO.getOwnerId());
|
innerReq.setOwnerId(reqVO.getOwnerId());
|
||||||
innerReq.setPriority(reqVO.getPriority());
|
innerReq.setPriority(reqVO.getPriority());
|
||||||
innerReq.setUpdateTime(reqVO.getUpdateTime());
|
innerReq.setUpdateTime(reqVO.getUpdateTime());
|
||||||
@@ -131,34 +131,24 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService
|
|||||||
return innerReq;
|
return innerReq;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算任务可见性 scope,与 ProjectTaskServiceImpl#computeTaskScope 同款:
|
|
||||||
* 项目经理 → seesAll;执行负责人 = 当前用户 → seesAll;否则按 resolveForExecution 求并集。
|
|
||||||
*/
|
|
||||||
private VisibilityScope resolveTaskScope(Long projectId, Long executionId) {
|
|
||||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
|
||||||
VisibilityScope scope = visibilityScopeResolver.resolveForExecution(projectId, executionId, userId);
|
|
||||||
if (scope.seesAll()) {
|
|
||||||
return scope;
|
|
||||||
}
|
|
||||||
ProjectExecutionDO exec = projectExecutionMapper.selectByProjectIdAndId(projectId, executionId);
|
|
||||||
if (exec != null && Objects.equals(exec.getOwnerId(), userId)) {
|
|
||||||
return VisibilityScope.all();
|
|
||||||
}
|
|
||||||
return scope;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ProjectExecutionStatusBoardRespVO buildExecutionStatusBoard(Long projectId,
|
private ProjectExecutionStatusBoardRespVO buildExecutionStatusBoard(Long projectId,
|
||||||
VisibilityScope scope,
|
|
||||||
ProjectExecutionStatusBoardReqVO reqVO,
|
ProjectExecutionStatusBoardReqVO reqVO,
|
||||||
List<ObjectStatusModelDO> statusModels) {
|
List<ObjectStatusModelDO> statusModels) {
|
||||||
|
// dueRange 截止时间过滤所需的日期边界与执行终态码(终态排除口径对齐任务 summary)。
|
||||||
|
LocalDate today = DueRangeSupport.today();
|
||||||
|
LocalDate weekStart = DueRangeSupport.weekStart(today);
|
||||||
|
LocalDate weekEnd = DueRangeSupport.weekEnd(today);
|
||||||
|
List<String> terminalStatusCodes = objectStatusModelMapper
|
||||||
|
.selectTerminalStatusCodesByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE);
|
||||||
|
|
||||||
ProjectExecutionStatusBoardRespVO respVO = new ProjectExecutionStatusBoardRespVO();
|
ProjectExecutionStatusBoardRespVO respVO = new ProjectExecutionStatusBoardRespVO();
|
||||||
List<ProjectExecutionStatusBoardRespVO.ProjectStatusBoardItemVO> items = statusModels.stream().map(statusModel -> {
|
List<ProjectExecutionStatusBoardRespVO.ProjectStatusBoardItemVO> items = statusModels.stream().map(statusModel -> {
|
||||||
ProjectExecutionStatusBoardRespVO.ProjectStatusBoardItemVO item =
|
ProjectExecutionStatusBoardRespVO.ProjectStatusBoardItemVO item =
|
||||||
new ProjectExecutionStatusBoardRespVO.ProjectStatusBoardItemVO();
|
new ProjectExecutionStatusBoardRespVO.ProjectStatusBoardItemVO();
|
||||||
item.setStatusCode(statusModel.getStatusCode());
|
item.setStatusCode(statusModel.getStatusCode());
|
||||||
item.setStatusName(statusModel.getStatusName());
|
item.setStatusName(statusModel.getStatusName());
|
||||||
item.setCount(projectExecutionMapper.countByProjectIdAndStatusCode(projectId, scope, reqVO, statusModel.getStatusCode()).longValue());
|
item.setCount(projectExecutionMapper.countByProjectIdAndStatusCode(projectId, reqVO,
|
||||||
|
statusModel.getStatusCode(), terminalStatusCodes, today, weekStart, weekEnd).longValue());
|
||||||
item.setSort(statusModel.getSort());
|
item.setSort(statusModel.getSort());
|
||||||
item.setTerminal(statusModel.getTerminalFlag());
|
item.setTerminal(statusModel.getTerminalFlag());
|
||||||
return item;
|
return item;
|
||||||
@@ -169,7 +159,6 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ProjectTaskStatusBoardRespVO buildTaskStatusBoard(Long projectId, Long executionId,
|
private ProjectTaskStatusBoardRespVO buildTaskStatusBoard(Long projectId, Long executionId,
|
||||||
VisibilityScope scope,
|
|
||||||
ProjectTaskStatusBoardReqVO reqVO,
|
ProjectTaskStatusBoardReqVO reqVO,
|
||||||
List<ObjectStatusModelDO> statusModels) {
|
List<ObjectStatusModelDO> statusModels) {
|
||||||
ProjectTaskStatusBoardRespVO respVO = new ProjectTaskStatusBoardRespVO();
|
ProjectTaskStatusBoardRespVO respVO = new ProjectTaskStatusBoardRespVO();
|
||||||
@@ -177,7 +166,7 @@ public class ProjectStatusBoardServiceImpl implements ProjectStatusBoardService
|
|||||||
ProjectTaskStatusBoardRespVO.ProjectStatusBoardItemVO item = new ProjectTaskStatusBoardRespVO.ProjectStatusBoardItemVO();
|
ProjectTaskStatusBoardRespVO.ProjectStatusBoardItemVO item = new ProjectTaskStatusBoardRespVO.ProjectStatusBoardItemVO();
|
||||||
item.setStatusCode(statusModel.getStatusCode());
|
item.setStatusCode(statusModel.getStatusCode());
|
||||||
item.setStatusName(statusModel.getStatusName());
|
item.setStatusName(statusModel.getStatusName());
|
||||||
item.setCount(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(projectId, executionId, scope, reqVO,
|
item.setCount(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(projectId, executionId, reqVO,
|
||||||
statusModel.getStatusCode()).longValue());
|
statusModel.getStatusCode()).longValue());
|
||||||
item.setSort(statusModel.getSort());
|
item.setSort(statusModel.getSort());
|
||||||
item.setTerminal(statusModel.getTerminalFlag());
|
item.setTerminal(statusModel.getTerminalFlag());
|
||||||
|
|||||||
@@ -41,11 +41,10 @@ import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
|||||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
|
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
|
||||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||||
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
|
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
|
||||||
|
import com.njcn.rdms.module.project.util.DueRangeSupport;
|
||||||
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
|
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
|
||||||
import com.njcn.rdms.module.project.service.project.ProjectRequirementService;
|
import com.njcn.rdms.module.project.service.project.ProjectRequirementService;
|
||||||
import com.njcn.rdms.module.project.service.project.ProjectService;
|
import com.njcn.rdms.module.project.service.project.ProjectService;
|
||||||
import com.njcn.rdms.module.project.service.project.permission.VisibilityScope;
|
|
||||||
import com.njcn.rdms.module.project.service.project.permission.VisibilityScopeResolver;
|
|
||||||
import com.njcn.rdms.module.project.service.project.task.ProjectTaskService;
|
import com.njcn.rdms.module.project.service.project.task.ProjectTaskService;
|
||||||
import com.njcn.rdms.module.system.api.dict.DictDataApi;
|
import com.njcn.rdms.module.system.api.dict.DictDataApi;
|
||||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||||
@@ -119,8 +118,6 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
|||||||
private ProjectRequirementService projectRequirementService;
|
private ProjectRequirementService projectRequirementService;
|
||||||
@Resource
|
@Resource
|
||||||
private ProjectRequirementMapper projectRequirementMapper;
|
private ProjectRequirementMapper projectRequirementMapper;
|
||||||
@Resource
|
|
||||||
private VisibilityScopeResolver visibilityScopeResolver;
|
|
||||||
/**
|
/**
|
||||||
* 任务服务:执行 cancel / pause / resume 时级联调任务侧 internal 入口。
|
* 任务服务:执行 cancel / pause / resume 时级联调任务侧 internal 入口。
|
||||||
* 与 ProjectTaskService 互相依赖(任务侧已注入 ProjectExecutionService 用于通知执行),用 @Lazy 打破循环。
|
* 与 ProjectTaskService 互相依赖(任务侧已注入 ProjectExecutionService 用于通知执行),用 @Lazy 打破循环。
|
||||||
@@ -209,24 +206,25 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectExecutionConstants.PERMISSION_QUERY)
|
||||||
public PageResult<ProjectExecutionDO> getExecutionPage(Long projectId, ProjectExecutionPageReqVO reqVO) {
|
public PageResult<ProjectExecutionDO> getExecutionPage(Long projectId, ProjectExecutionPageReqVO reqVO) {
|
||||||
validateProjectExists(projectId);
|
validateProjectExists(projectId);
|
||||||
// 数据可见性:项目经理看全部;非经理按"我 owner 的执行 ∪ 我活跃协办的执行"过滤
|
// "我参与 / 所有"视角完全由前端发不发 reqVO.involveUserId 决定。
|
||||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
// 注:getExecutionRespVOPage 内部 this.getExecutionPage() 自调用不触发 AOP,但外层注解已守门;
|
||||||
VisibilityScope scope = visibilityScopeResolver.resolveForProject(projectId, userId);
|
// 此处独立挂注解是为了堵跨 service 直调 ProjectExecutionService.getExecutionPage 的鉴权后门。
|
||||||
return projectExecutionMapper.selectPageByProjectId(projectId, scope, reqVO);
|
// dueRange 截止时间过滤所需的日期边界与执行终态码(终态排除口径对齐任务 summary)。
|
||||||
|
LocalDate today = DueRangeSupport.today();
|
||||||
|
return projectExecutionMapper.selectPageByProjectId(projectId, reqVO,
|
||||||
|
loadExecutionTerminalStatusCodes(), today,
|
||||||
|
DueRangeSupport.weekStart(today), DueRangeSupport.weekEnd(today));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectExecutionConstants.PERMISSION_QUERY)
|
||||||
public ProjectExecutionRespVO getExecutionRespVO(Long projectId, Long executionId) {
|
public ProjectExecutionRespVO getExecutionRespVO(Long projectId, Long executionId) {
|
||||||
ProjectExecutionDO execution = getExecution(projectId, executionId);
|
ProjectExecutionDO execution = getExecution(projectId, executionId);
|
||||||
// 可见性卡断:项目经理放行;否则 executionId 必须在 scope.executionIds 中。
|
|
||||||
// 未命中按"执行不存在"语义返回,不暴露存在性。
|
|
||||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
|
||||||
VisibilityScope scope = visibilityScopeResolver.resolveForProject(projectId, userId);
|
|
||||||
if (!scope.seesAll() && !scope.executionIds().contains(executionId)) {
|
|
||||||
throw exception(ErrorCodeConstants.PROJECT_EXECUTION_NOT_EXISTS);
|
|
||||||
}
|
|
||||||
ProjectExecutionRespVO respVO = BeanUtils.toBean(execution, ProjectExecutionRespVO.class);
|
ProjectExecutionRespVO respVO = BeanUtils.toBean(execution, ProjectExecutionRespVO.class);
|
||||||
respVO.setProgressRate(loadExecutionProgress(projectId, executionId));
|
respVO.setProgressRate(loadExecutionProgress(projectId, executionId));
|
||||||
boolean rootTasksAllCompleted = loadExecutionRootTasksAllCompleted(projectId, executionId);
|
boolean rootTasksAllCompleted = loadExecutionRootTasksAllCompleted(projectId, executionId);
|
||||||
@@ -237,6 +235,8 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectExecutionConstants.PERMISSION_QUERY)
|
||||||
public PageResult<ProjectExecutionRespVO> getExecutionRespVOPage(Long projectId, ProjectExecutionPageReqVO reqVO) {
|
public PageResult<ProjectExecutionRespVO> getExecutionRespVOPage(Long projectId, ProjectExecutionPageReqVO reqVO) {
|
||||||
PageResult<ProjectExecutionDO> pageResult = getExecutionPage(projectId, reqVO);
|
PageResult<ProjectExecutionDO> pageResult = getExecutionPage(projectId, reqVO);
|
||||||
PageResult<ProjectExecutionRespVO> voPageResult = BeanUtils.toBean(pageResult, ProjectExecutionRespVO.class);
|
PageResult<ProjectExecutionRespVO> voPageResult = BeanUtils.toBean(pageResult, ProjectExecutionRespVO.class);
|
||||||
@@ -859,6 +859,13 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
|||||||
return statusCodes == null ? Collections.emptyList() : statusCodes;
|
return statusCodes == null ? Collections.emptyList() : statusCodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** dueRange 终态排除用:执行对象域的终态码(动态查,不硬编码)。 */
|
||||||
|
private List<String> loadExecutionTerminalStatusCodes() {
|
||||||
|
List<String> statusCodes = objectStatusModelMapper
|
||||||
|
.selectTerminalStatusCodesByObjectTypeEnabled(ProjectExecutionConstants.OBJECT_TYPE);
|
||||||
|
return statusCodes == null ? Collections.emptyList() : statusCodes;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行详情完成态:判断"参与聚合的根任务"是否全部为 completed。
|
* 执行详情完成态:判断"参与聚合的根任务"是否全部为 completed。
|
||||||
* 筛选口径与 loadExecutionProgress 同源;空集(无参与聚合的根任务)返回 false,禁止下发 complete。
|
* 筛选口径与 loadExecutionProgress 同源;空集(无参与聚合的根任务)返回 false,禁止下发 complete。
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
package com.njcn.rdms.module.project.service.project.permission;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 数据可见性 scope,由 VisibilityScopeResolver 计算得出。
|
|
||||||
* - seesAll=true:项目经理等"看全部"角色,分页/计数 SQL 跳过任何 ID 过滤
|
|
||||||
* - seesAll=false:仅命中 executionIds / taskIds 的数据可见;集合为空 = 完全不可见
|
|
||||||
*
|
|
||||||
* 实例不可变;空集合用 Set.of() 表达,调用方不得修改。
|
|
||||||
*/
|
|
||||||
public record VisibilityScope(
|
|
||||||
boolean seesAll,
|
|
||||||
Set<Long> executionIds,
|
|
||||||
Set<Long> taskIds
|
|
||||||
) {
|
|
||||||
|
|
||||||
public static VisibilityScope all() {
|
|
||||||
return new VisibilityScope(true, Set.of(), Set.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static VisibilityScope of(Set<Long> executionIds, Set<Long> taskIds) {
|
|
||||||
return new VisibilityScope(false,
|
|
||||||
executionIds == null ? Set.of() : Set.copyOf(executionIds),
|
|
||||||
taskIds == null ? Set.of() : Set.copyOf(taskIds));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static VisibilityScope empty() {
|
|
||||||
return new VisibilityScope(false, Set.of(), Set.of());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package com.njcn.rdms.module.project.service.project.permission;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算当前登录用户在某项目 / 某执行下的数据可见性 scope。
|
|
||||||
*
|
|
||||||
* 规则:
|
|
||||||
* - 项目经理(project.manager_user_id == userId)→ seesAll=true
|
|
||||||
* - 非项目经理 → 取以下 4 项的并集,构成 (executionIds, taskIds):
|
|
||||||
* 1. 我作为 execution.owner_id 的执行 ID
|
|
||||||
* 2. 我作为 execution_assignee 活跃协办的执行 ID(removed_at IS NULL)
|
|
||||||
* 3. 我作为 task.owner_id 的任务 ID 及其全部子孙 ID(递归 CTE 一次展开)
|
|
||||||
* 4. 我作为 task_assignee 活跃协办的任务 ID(removed_at IS NULL)
|
|
||||||
*
|
|
||||||
* 任务参与者集合 ⊆ 执行参与者集合(业务约束:任务负责人/协办人必须从执行团队挑选)。
|
|
||||||
*/
|
|
||||||
public interface VisibilityScopeResolver {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 项目维度 scope(用于执行分页 / 执行看板)。
|
|
||||||
*/
|
|
||||||
VisibilityScope resolveForProject(Long projectId, Long userId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行维度 scope(用于任务分页 / 任务看板 / 任务详情)。
|
|
||||||
* 调用方需先保证 executionId 属于 projectId(由 URL 路径约束)。
|
|
||||||
*/
|
|
||||||
VisibilityScope resolveForExecution(Long projectId, Long executionId, Long userId);
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package com.njcn.rdms.module.project.service.project.permission;
|
|
||||||
|
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.execution.ExecutionAssigneeMapper;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.task.TaskAssigneeMapper;
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* VisibilityScopeResolver 实现。
|
|
||||||
*
|
|
||||||
* 短路:project.manager_user_id == userId → seesAll=true,跳过任何 ID 过滤。
|
|
||||||
* 非项目经理:并集 4 个 Mapper 来源得到可见的 executionIds / taskIds。
|
|
||||||
*
|
|
||||||
* 任务的"执行 owner 看执行下所有任务"短路不在此处实现,
|
|
||||||
* 由 ProjectTaskServiceImpl / ProjectStatusBoardServiceImpl 在调用本 Resolver 前自行判定。
|
|
||||||
* 本 Resolver 仅负责"参与者 → 可见 ID 集合"的纯查询。
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
public class VisibilityScopeResolverImpl implements VisibilityScopeResolver {
|
|
||||||
|
|
||||||
@Resource
|
|
||||||
private ProjectMapper projectMapper;
|
|
||||||
@Resource
|
|
||||||
private ProjectExecutionMapper projectExecutionMapper;
|
|
||||||
@Resource
|
|
||||||
private ExecutionAssigneeMapper executionAssigneeMapper;
|
|
||||||
@Resource
|
|
||||||
private ProjectTaskMapper projectTaskMapper;
|
|
||||||
@Resource
|
|
||||||
private TaskAssigneeMapper taskAssigneeMapper;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public VisibilityScope resolveForProject(Long projectId, Long userId) {
|
|
||||||
if (isProjectManager(projectId, userId)) {
|
|
||||||
return VisibilityScope.all();
|
|
||||||
}
|
|
||||||
Set<Long> executionIds = new LinkedHashSet<>();
|
|
||||||
executionIds.addAll(projectExecutionMapper.selectIdsByProjectIdAndOwnerId(projectId, userId));
|
|
||||||
executionIds.addAll(executionAssigneeMapper.selectActiveExecutionIdsByProjectIdAndUserId(projectId, userId));
|
|
||||||
|
|
||||||
Set<Long> taskIds = new LinkedHashSet<>();
|
|
||||||
taskIds.addAll(projectTaskMapper.selectOwnedTaskAndDescendantIdsByProjectIdAndUserId(projectId, userId));
|
|
||||||
taskIds.addAll(taskAssigneeMapper.selectActiveTaskIdsByProjectIdAndUserId(projectId, userId));
|
|
||||||
|
|
||||||
return VisibilityScope.of(executionIds, taskIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public VisibilityScope resolveForExecution(Long projectId, Long executionId, Long userId) {
|
|
||||||
if (isProjectManager(projectId, userId)) {
|
|
||||||
return VisibilityScope.all();
|
|
||||||
}
|
|
||||||
// executionIds 在执行维度无用,统一传空集;调用方靠 taskIds 过滤分页/计数。
|
|
||||||
Set<Long> taskIds = new LinkedHashSet<>();
|
|
||||||
taskIds.addAll(projectTaskMapper.selectOwnedTaskAndDescendantIdsByExecutionIdAndUserId(projectId, executionId, userId));
|
|
||||||
taskIds.addAll(taskAssigneeMapper.selectActiveTaskIdsByProjectIdAndExecutionIdAndUserId(projectId, executionId, userId));
|
|
||||||
|
|
||||||
return VisibilityScope.of(Set.of(), taskIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isProjectManager(Long projectId, Long userId) {
|
|
||||||
if (projectId == null || userId == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ProjectDO project = projectMapper.selectById(projectId);
|
|
||||||
return project != null && Objects.equals(project.getManagerUserId(), userId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.njcn.rdms.module.project.service.project.task;
|
||||||
|
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskBoardPageRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateBoardPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregatePageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateStatusBoardReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskSummaryReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskSummaryRespVO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目级跨执行任务查询 Service(与执行级 {@link ProjectTaskService} 互补)。
|
||||||
|
* 服务于"我的任务"、"项目全部任务"两种跨执行视角。
|
||||||
|
*/
|
||||||
|
public interface ProjectTaskAggregateService {
|
||||||
|
|
||||||
|
PageResult<ProjectTaskRespVO> getAggregateTaskPage(Long projectId, ProjectTaskAggregatePageReqVO reqVO);
|
||||||
|
|
||||||
|
ProjectTaskStatusBoardRespVO getAggregateTaskStatusBoard(Long projectId, ProjectTaskAggregateStatusBoardReqVO reqVO);
|
||||||
|
|
||||||
|
ProjectTaskBoardPageRespVO getAggregateTaskBoardPage(Long projectId, ProjectTaskAggregateBoardPageReqVO reqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跨执行任务今日小条:
|
||||||
|
* 入参 involveUserId 为 null → 项目内全部任务;
|
||||||
|
* involveUserId 不为 null → 限定 owner 或活跃协办为该用户。
|
||||||
|
* 由 @CheckObjectPermission(project:task:query) 守门,无权限直接 403。
|
||||||
|
*/
|
||||||
|
ProjectTaskSummaryRespVO getAggregateTaskSummary(Long projectId, ProjectTaskSummaryReqVO reqVO);
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
package com.njcn.rdms.module.project.service.project.task;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
|
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
||||||
|
import com.njcn.rdms.module.project.constant.ProjectTaskConstants;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskBoardPageRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardRespVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateBoardPageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregatePageReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateStatusBoardReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskSummaryReqVO;
|
||||||
|
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskSummaryRespVO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
|
||||||
|
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
|
||||||
|
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
||||||
|
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.DayOfWeek;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.temporal.TemporalAdjusters;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class ProjectTaskAggregateServiceImpl implements ProjectTaskAggregateService {
|
||||||
|
|
||||||
|
private static final ZoneId SERVER_ZONE = ZoneId.of("Asia/Shanghai");
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ProjectTaskMapper projectTaskMapper;
|
||||||
|
@Resource
|
||||||
|
private ProjectTaskService projectTaskService;
|
||||||
|
@Resource
|
||||||
|
private ObjectStatusModelMapper objectStatusModelMapper;
|
||||||
|
|
||||||
|
// ========= 公共 helper =========
|
||||||
|
|
||||||
|
private LocalDate today() {
|
||||||
|
return LocalDate.now(SERVER_ZONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDate weekStart(LocalDate today) {
|
||||||
|
return today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDate weekEnd(LocalDate today) {
|
||||||
|
return today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> loadTerminalStatusCodes() {
|
||||||
|
return objectStatusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= page =========
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectTaskConstants.PERMISSION_QUERY)
|
||||||
|
public PageResult<ProjectTaskRespVO> getAggregateTaskPage(Long projectId, ProjectTaskAggregatePageReqVO reqVO) {
|
||||||
|
// 空数组语义短路:前端明确"按 0 个执行状态/执行 id 过滤" → 返空集合,不让 MyBatis 退化成"不过滤"
|
||||||
|
if ((reqVO.getExecutionStatusCodes() != null && reqVO.getExecutionStatusCodes().isEmpty())
|
||||||
|
|| (reqVO.getExecutionIds() != null && reqVO.getExecutionIds().isEmpty())) {
|
||||||
|
return PageResult.empty();
|
||||||
|
}
|
||||||
|
LocalDate today = today();
|
||||||
|
LocalDate weekStart = weekStart(today);
|
||||||
|
LocalDate weekEnd = weekEnd(today);
|
||||||
|
List<String> terminalStatusCodes = loadTerminalStatusCodes();
|
||||||
|
|
||||||
|
Page<ProjectTaskDO> page = new Page<>(reqVO.getPageNo(), reqVO.getPageSize());
|
||||||
|
IPage<ProjectTaskDO> ipage = projectTaskMapper.selectAggregatePageByProjectId(
|
||||||
|
projectId, reqVO, terminalStatusCodes, weekStart, weekEnd, today, page);
|
||||||
|
PageResult<ProjectTaskDO> doPage = new PageResult<>(ipage.getRecords(), ipage.getTotal());
|
||||||
|
return projectTaskService.assembleTaskRespVOPageCrossExecution(projectId, doPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= status-board =========
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectTaskConstants.PERMISSION_QUERY)
|
||||||
|
public ProjectTaskStatusBoardRespVO getAggregateTaskStatusBoard(Long projectId, ProjectTaskAggregateStatusBoardReqVO reqVO) {
|
||||||
|
// 空数组语义短路:跳过 SQL,但保留状态列骨架(前端看板依赖全列表渲染),count 全部 0
|
||||||
|
if ((reqVO.getExecutionStatusCodes() != null && reqVO.getExecutionStatusCodes().isEmpty())
|
||||||
|
|| (reqVO.getExecutionIds() != null && reqVO.getExecutionIds().isEmpty())) {
|
||||||
|
List<ObjectStatusModelDO> emptyStatusModels =
|
||||||
|
objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
||||||
|
return buildStatusBoardResponse(emptyStatusModels, Collections.emptyMap());
|
||||||
|
}
|
||||||
|
LocalDate today = today();
|
||||||
|
LocalDate weekStart = weekStart(today);
|
||||||
|
LocalDate weekEnd = weekEnd(today);
|
||||||
|
List<String> terminalStatusCodes = loadTerminalStatusCodes();
|
||||||
|
|
||||||
|
List<ObjectStatusModelDO> statusModels =
|
||||||
|
objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
||||||
|
|
||||||
|
List<ProjectTaskMapper.StatusCountRow> rows = projectTaskMapper.selectAggregateStatusCount(
|
||||||
|
projectId, reqVO, terminalStatusCodes, weekStart, weekEnd, today);
|
||||||
|
Map<String, Long> countMap = rows.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
ProjectTaskMapper.StatusCountRow::getStatusCode,
|
||||||
|
ProjectTaskMapper.StatusCountRow::getCount));
|
||||||
|
|
||||||
|
return buildStatusBoardResponse(statusModels, countMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按状态模型全量列出,count=0 的状态也输出(前端看板始终显示所有列)。
|
||||||
|
*/
|
||||||
|
private ProjectTaskStatusBoardRespVO buildStatusBoardResponse(
|
||||||
|
List<ObjectStatusModelDO> statusModels, Map<String, Long> countMap) {
|
||||||
|
List<ProjectTaskStatusBoardRespVO.ProjectStatusBoardItemVO> items = statusModels.stream()
|
||||||
|
.map(sm -> {
|
||||||
|
ProjectTaskStatusBoardRespVO.ProjectStatusBoardItemVO item =
|
||||||
|
new ProjectTaskStatusBoardRespVO.ProjectStatusBoardItemVO();
|
||||||
|
item.setStatusCode(sm.getStatusCode());
|
||||||
|
item.setStatusName(sm.getStatusName());
|
||||||
|
item.setCount(countMap.getOrDefault(sm.getStatusCode(), 0L));
|
||||||
|
item.setSort(sm.getSort());
|
||||||
|
item.setTerminal(sm.getTerminalFlag());
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
long total = items.stream().mapToLong(i -> i.getCount() == null ? 0L : i.getCount()).sum();
|
||||||
|
|
||||||
|
ProjectTaskStatusBoardRespVO resp = new ProjectTaskStatusBoardRespVO();
|
||||||
|
resp.setTotal(total);
|
||||||
|
resp.setItems(items);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= board-page =========
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectTaskConstants.PERMISSION_QUERY)
|
||||||
|
public ProjectTaskBoardPageRespVO getAggregateTaskBoardPage(Long projectId, ProjectTaskAggregateBoardPageReqVO reqVO) {
|
||||||
|
// 空数组语义短路:executionStatusCodes 或 executionIds 为空数组 → 该范围明确为空,跳过 SQL,保留列骨架(每列 list=[] / total=0)
|
||||||
|
boolean emptyExecScope = (reqVO.getExecutionStatusCodes() != null && reqVO.getExecutionStatusCodes().isEmpty())
|
||||||
|
|| (reqVO.getExecutionIds() != null && reqVO.getExecutionIds().isEmpty());
|
||||||
|
|
||||||
|
List<ObjectStatusModelDO> allStatusModels =
|
||||||
|
objectStatusModelMapper.selectListByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
|
||||||
|
List<ObjectStatusModelDO> targetStatusModels;
|
||||||
|
if (reqVO.getStatusCodes() == null || reqVO.getStatusCodes().isEmpty()) {
|
||||||
|
targetStatusModels = allStatusModels;
|
||||||
|
} else {
|
||||||
|
Set<String> filter = new HashSet<>(reqVO.getStatusCodes());
|
||||||
|
targetStatusModels = allStatusModels.stream()
|
||||||
|
.filter(sm -> filter.contains(sm.getStatusCode()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emptyExecScope) {
|
||||||
|
List<ProjectTaskBoardPageRespVO.ColumnItemVO> emptyItems = targetStatusModels.stream()
|
||||||
|
.map(sm -> buildColumnItemVO(sm, PageResult.empty()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
ProjectTaskBoardPageRespVO emptyResp = new ProjectTaskBoardPageRespVO();
|
||||||
|
emptyResp.setItems(emptyItems);
|
||||||
|
return emptyResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate today = today();
|
||||||
|
LocalDate weekStart = weekStart(today);
|
||||||
|
LocalDate weekEnd = weekEnd(today);
|
||||||
|
List<String> terminalStatusCodes = loadTerminalStatusCodes();
|
||||||
|
|
||||||
|
List<ProjectTaskBoardPageRespVO.ColumnItemVO> items = targetStatusModels.stream()
|
||||||
|
.map(sm -> buildAggregateBoardColumn(projectId, reqVO, sm,
|
||||||
|
terminalStatusCodes, today, weekStart, weekEnd))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
ProjectTaskBoardPageRespVO resp = new ProjectTaskBoardPageRespVO();
|
||||||
|
resp.setItems(items);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProjectTaskBoardPageRespVO.ColumnItemVO buildAggregateBoardColumn(
|
||||||
|
Long projectId,
|
||||||
|
ProjectTaskAggregateBoardPageReqVO reqVO, ObjectStatusModelDO sm,
|
||||||
|
List<String> terminalStatusCodes, LocalDate today, LocalDate weekStart, LocalDate weekEnd) {
|
||||||
|
|
||||||
|
// 复用 selectAggregatePageByProjectId,构造单列过滤的 innerReq
|
||||||
|
ProjectTaskAggregatePageReqVO innerReq = new ProjectTaskAggregatePageReqVO();
|
||||||
|
innerReq.setPageNo(reqVO.getPageNo());
|
||||||
|
innerReq.setPageSize(reqVO.getPageSize());
|
||||||
|
innerReq.setKeyword(reqVO.getKeyword());
|
||||||
|
innerReq.setExecutionIds(reqVO.getExecutionIds());
|
||||||
|
innerReq.setExecutionStatusCodes(reqVO.getExecutionStatusCodes());
|
||||||
|
innerReq.setInvolveUserId(reqVO.getInvolveUserId());
|
||||||
|
innerReq.setExecutionInvolveUserId(reqVO.getExecutionInvolveUserId());
|
||||||
|
innerReq.setOwnerId(reqVO.getOwnerId());
|
||||||
|
innerReq.setStatusCodes(Collections.singletonList(sm.getStatusCode()));
|
||||||
|
innerReq.setPriority(reqVO.getPriority());
|
||||||
|
innerReq.setParentTaskId(reqVO.getParentTaskId());
|
||||||
|
innerReq.setDueRange(reqVO.getDueRange());
|
||||||
|
innerReq.setUpdateTime(reqVO.getUpdateTime());
|
||||||
|
// board-page 不传 sortBy / sortOrder,用 page 查询的默认排序(plannedEndDate ASC)
|
||||||
|
|
||||||
|
Page<ProjectTaskDO> page = new Page<>(innerReq.getPageNo(), innerReq.getPageSize());
|
||||||
|
IPage<ProjectTaskDO> ipage = projectTaskMapper.selectAggregatePageByProjectId(
|
||||||
|
projectId, innerReq, terminalStatusCodes, weekStart, weekEnd, today, page);
|
||||||
|
PageResult<ProjectTaskDO> doPage = new PageResult<>(ipage.getRecords(), ipage.getTotal());
|
||||||
|
PageResult<ProjectTaskRespVO> voPage =
|
||||||
|
projectTaskService.assembleTaskRespVOPageCrossExecution(projectId, doPage);
|
||||||
|
|
||||||
|
return buildColumnItemVO(sm, voPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProjectTaskBoardPageRespVO.ColumnItemVO buildColumnItemVO(
|
||||||
|
ObjectStatusModelDO sm, PageResult<ProjectTaskRespVO> voPage) {
|
||||||
|
ProjectTaskBoardPageRespVO.ColumnItemVO col = new ProjectTaskBoardPageRespVO.ColumnItemVO();
|
||||||
|
col.setStatusCode(sm.getStatusCode());
|
||||||
|
col.setStatusName(sm.getStatusName());
|
||||||
|
col.setSort(sm.getSort());
|
||||||
|
col.setTerminal(sm.getTerminalFlag());
|
||||||
|
col.setList(voPage.getList());
|
||||||
|
col.setTotal(voPage.getTotal());
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========= summary =========
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectTaskConstants.PERMISSION_QUERY)
|
||||||
|
public ProjectTaskSummaryRespVO getAggregateTaskSummary(Long projectId, ProjectTaskSummaryReqVO reqVO) {
|
||||||
|
LocalDate today = today();
|
||||||
|
LocalDate weekStart = weekStart(today);
|
||||||
|
LocalDate weekEnd = weekEnd(today);
|
||||||
|
List<String> terminalStatusCodes = loadTerminalStatusCodes();
|
||||||
|
|
||||||
|
Map<String, Long> counts = projectTaskMapper.selectAggregateSummaryCounts(
|
||||||
|
projectId,
|
||||||
|
reqVO.getInvolveUserId(),
|
||||||
|
terminalStatusCodes, ProjectTaskConstants.STATUS_COMPLETED,
|
||||||
|
today, weekStart, weekEnd);
|
||||||
|
|
||||||
|
ProjectTaskSummaryRespVO resp = new ProjectTaskSummaryRespVO();
|
||||||
|
resp.setOverdue(zeroIfNull(counts.get("overdue")));
|
||||||
|
resp.setDueToday(zeroIfNull(counts.get("dueToday")));
|
||||||
|
resp.setDueThisWeek(zeroIfNull(counts.get("dueThisWeek")));
|
||||||
|
resp.setDoneThisWeek(zeroIfNull(counts.get("doneThisWeek")));
|
||||||
|
resp.setToday(today);
|
||||||
|
resp.setWeekStart(weekStart);
|
||||||
|
resp.setWeekEnd(weekEnd);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long zeroIfNull(Long v) {
|
||||||
|
return v == null ? 0L : v;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,18 @@ public interface ProjectTaskService {
|
|||||||
PageResult<ProjectTaskRespVO> assembleTaskRespVOPage(Long projectId, Long executionId,
|
PageResult<ProjectTaskRespVO> assembleTaskRespVOPage(Long projectId, Long executionId,
|
||||||
PageResult<ProjectTaskDO> doPage);
|
PageResult<ProjectTaskDO> doPage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跨执行装配 ProjectTaskRespVO 分页结果。
|
||||||
|
*
|
||||||
|
* 与 {@link #assembleTaskRespVOPage(Long, Long, PageResult)} 区别:
|
||||||
|
* 本方法不绑单个 executionId,允许 page 内任务来自项目下任意多个执行;
|
||||||
|
* 装配时按 task.executionId 分组批量回填 executionName / executionStatusCode
|
||||||
|
* (走 enrichExecutionInfo helper)。
|
||||||
|
* <p><b>不填充字段</b>:executionOwnerId / projectRequirementName / projectRequirementStatusCode
|
||||||
|
* (跨多 execution 场景无法共享单 execution 上下文)。前端跨执行视图按需处理。</p>
|
||||||
|
*/
|
||||||
|
PageResult<ProjectTaskRespVO> assembleTaskRespVOPageCrossExecution(Long projectId, PageResult<ProjectTaskDO> doPage);
|
||||||
|
|
||||||
void changeTaskStatus(Long projectId, Long executionId, Long taskId, ProjectTaskStatusActionReqVO reqVO);
|
void changeTaskStatus(Long projectId, Long executionId, Long taskId, ProjectTaskStatusActionReqVO reqVO);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.njcn.rdms.module.project.service.project.task;
|
package com.njcn.rdms.module.project.service.project.task;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||||
import com.njcn.rdms.framework.common.util.json.JsonUtils;
|
import com.njcn.rdms.framework.common.util.json.JsonUtils;
|
||||||
@@ -44,8 +45,6 @@ import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPer
|
|||||||
import com.njcn.rdms.module.project.framework.security.service.ProjectObjectAuthorizationService;
|
import com.njcn.rdms.module.project.framework.security.service.ProjectObjectAuthorizationService;
|
||||||
import com.njcn.rdms.module.project.service.project.ProjectService;
|
import com.njcn.rdms.module.project.service.project.ProjectService;
|
||||||
import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionService;
|
import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionService;
|
||||||
import com.njcn.rdms.module.project.service.project.permission.VisibilityScope;
|
|
||||||
import com.njcn.rdms.module.project.service.project.permission.VisibilityScopeResolver;
|
|
||||||
import com.njcn.rdms.module.project.service.project.task.assignee.TaskAssigneeService;
|
import com.njcn.rdms.module.project.service.project.task.assignee.TaskAssigneeService;
|
||||||
import com.njcn.rdms.module.project.service.project.task.worklog.TaskWorklogService;
|
import com.njcn.rdms.module.project.service.project.task.worklog.TaskWorklogService;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
@@ -62,6 +61,7 @@ import org.springframework.util.StringUtils;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.ZoneId;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -85,6 +85,8 @@ import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class ProjectTaskServiceImpl implements ProjectTaskService {
|
public class ProjectTaskServiceImpl implements ProjectTaskService {
|
||||||
|
|
||||||
|
private static final ZoneId SERVER_ZONE = ZoneId.of("Asia/Shanghai");
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ProjectMapper projectMapper;
|
private ProjectMapper projectMapper;
|
||||||
@Resource
|
@Resource
|
||||||
@@ -124,8 +126,6 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
@Resource
|
@Resource
|
||||||
private ProjectObjectAuthorizationService projectObjectAuthorizationService;
|
private ProjectObjectAuthorizationService projectObjectAuthorizationService;
|
||||||
@Resource
|
@Resource
|
||||||
private VisibilityScopeResolver visibilityScopeResolver;
|
|
||||||
@Resource
|
|
||||||
private DictDataApi dictDataApi;
|
private DictDataApi dictDataApi;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -173,6 +173,9 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
if (task.getParentTaskId() != null) {
|
if (task.getParentTaskId() != null) {
|
||||||
recalcParentProgressFrom(task.getParentTaskId());
|
recalcParentProgressFrom(task.getParentTaskId());
|
||||||
}
|
}
|
||||||
|
// 项目进度推算:无论新建的是根任务(进度=0 进入项目均值)还是子任务(冒泡后根任务进度可能变化),
|
||||||
|
// 项目均值都可能发生变化,统一触发一次项目层重算(覆盖根任务边界)
|
||||||
|
projectService.recalcProgress(projectId);
|
||||||
|
|
||||||
writeTaskAuditLog(task, ObjectActivityConstants.TASK_ACTION_CREATE, null, task.getStatusCode(),
|
writeTaskAuditLog(task, ObjectActivityConstants.TASK_ACTION_CREATE, null, task.getStatusCode(),
|
||||||
buildTaskFieldChanges(null, task), null);
|
buildTaskFieldChanges(null, task), null);
|
||||||
@@ -238,6 +241,9 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
parentsToRecalc.add(newParentId);
|
parentsToRecalc.add(newParentId);
|
||||||
}
|
}
|
||||||
parentsToRecalc.forEach(this::recalcParentProgressFrom);
|
parentsToRecalc.forEach(this::recalcParentProgressFrom);
|
||||||
|
// 项目进度推算:updateTask 可能改变 parent_task_id,使 task 进入/离开"根任务集合",
|
||||||
|
// 项目均值的分量构成可能变化;无论 progressRate 是否变化都需触发刷新
|
||||||
|
projectService.recalcProgress(projectId);
|
||||||
|
|
||||||
writeTaskAuditLog(task, ObjectActivityConstants.TASK_ACTION_UPDATE, before.getStatusCode(),
|
writeTaskAuditLog(task, ObjectActivityConstants.TASK_ACTION_UPDATE, before.getStatusCode(),
|
||||||
task.getStatusCode(), buildTaskFieldChanges(before, task), null);
|
task.getStatusCode(), buildTaskFieldChanges(before, task), null);
|
||||||
@@ -281,6 +287,9 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
if (parentTaskId != null) {
|
if (parentTaskId != null) {
|
||||||
recalcParentProgressFrom(parentTaskId);
|
recalcParentProgressFrom(parentTaskId);
|
||||||
}
|
}
|
||||||
|
// 项目进度推算:无论删除的是根任务(项目分量减一)还是子任务(父链冒泡到根后根任务进度变化),
|
||||||
|
// 项目均值都可能发生变化,统一触发一次项目层重算(覆盖根任务边界)
|
||||||
|
projectService.recalcProgress(task.getProjectId());
|
||||||
|
|
||||||
writeTaskAuditLog(task, ObjectActivityConstants.TASK_ACTION_DELETE, fromStatus, null, null, reason);
|
writeTaskAuditLog(task, ObjectActivityConstants.TASK_ACTION_DELETE, fromStatus, null, null, reason);
|
||||||
}
|
}
|
||||||
@@ -361,6 +370,8 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
if (task.getParentTaskId() != null) {
|
if (task.getParentTaskId() != null) {
|
||||||
recalcParentProgressFrom(task.getParentTaskId());
|
recalcParentProgressFrom(task.getParentTaskId());
|
||||||
}
|
}
|
||||||
|
// 项目进度推算:worklog 驱动的任务进度变化(叶子任务),无论叶子任务是否同时是根任务都需触发
|
||||||
|
projectService.recalcProgress(task.getProjectId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -417,44 +428,23 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectTaskConstants.PERMISSION_QUERY)
|
||||||
public PageResult<ProjectTaskDO> getTaskPage(Long projectId, Long executionId, ProjectTaskPageReqVO reqVO) {
|
public PageResult<ProjectTaskDO> getTaskPage(Long projectId, Long executionId, ProjectTaskPageReqVO reqVO) {
|
||||||
ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
|
validateExecutionExists(projectId, executionId);
|
||||||
VisibilityScope scope = computeTaskScope(projectId, executionId, execution);
|
// 进得来 = 看执行下全部任务,"我参与 / 所有"由前端发不发 reqVO.ownerId / involveUserId 决定。
|
||||||
return projectTaskMapper.selectPageByExecutionId(projectId, executionId, scope, reqVO);
|
// 注:getTaskRespVOPage 内部 this.getTaskPage() 自调用不触发 AOP,但外层注解已守门;
|
||||||
}
|
// 此处独立挂注解是为了堵跨 service 直调 ProjectTaskService.getTaskPage 的鉴权后门。
|
||||||
|
return projectTaskMapper.selectPageByExecutionId(projectId, executionId, reqVO);
|
||||||
/**
|
|
||||||
* 任务可见性计算:
|
|
||||||
* - 项目经理 → seesAll(由 Resolver 内置判定)
|
|
||||||
* - 执行负责人 = 当前用户 → seesAll(看本执行下全部任务)
|
|
||||||
* - 否则 → resolveForExecution 求并集(我 owner 的任务及子孙 ∪ 我活跃协办的任务)
|
|
||||||
*/
|
|
||||||
private VisibilityScope computeTaskScope(Long projectId, Long executionId, ProjectExecutionDO execution) {
|
|
||||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
|
||||||
VisibilityScope scope = visibilityScopeResolver.resolveForExecution(projectId, executionId, userId);
|
|
||||||
if (scope.seesAll()) {
|
|
||||||
return scope;
|
|
||||||
}
|
|
||||||
if (execution != null && Objects.equals(execution.getOwnerId(), userId)) {
|
|
||||||
return VisibilityScope.all();
|
|
||||||
}
|
|
||||||
return scope;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectTaskConstants.PERMISSION_QUERY)
|
||||||
public ProjectTaskRespVO getTaskRespVO(Long projectId, Long executionId, Long taskId) {
|
public ProjectTaskRespVO getTaskRespVO(Long projectId, Long executionId, Long taskId) {
|
||||||
// 内联 validate,便于接住 execution 供前端 executionOwnerId 字段使用
|
// 内联 validate,便于接住 execution 供前端 executionOwnerId 字段使用
|
||||||
ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
|
ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
|
||||||
ProjectTaskDO task = validateTaskExists(projectId, executionId, taskId);
|
ProjectTaskDO task = validateTaskExists(projectId, executionId, taskId);
|
||||||
// 可见性卡断:执行 owner / 项目经理直接放行;否则 taskId 必须在 scope.taskIds 中。
|
|
||||||
// 未命中按"任务不存在"语义返回,不暴露存在性。
|
|
||||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
|
||||||
if (!Objects.equals(execution.getOwnerId(), userId)) {
|
|
||||||
VisibilityScope scope = visibilityScopeResolver.resolveForExecution(projectId, executionId, userId);
|
|
||||||
if (!scope.seesAll() && !scope.taskIds().contains(taskId)) {
|
|
||||||
throw exception(ErrorCodeConstants.PROJECT_TASK_NOT_EXISTS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ProjectTaskRespVO respVO = BeanUtils.toBean(task, ProjectTaskRespVO.class);
|
ProjectTaskRespVO respVO = BeanUtils.toBean(task, ProjectTaskRespVO.class);
|
||||||
applyLifecycle(respVO);
|
applyLifecycle(respVO);
|
||||||
respVO.setOwnerNickname(loadOwnerNickname(task.getOwnerId()));
|
respVO.setOwnerNickname(loadOwnerNickname(task.getOwnerId()));
|
||||||
@@ -462,6 +452,8 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
.loadActiveAssigneesGroupedByTaskId(List.of(task.getId())).getOrDefault(task.getId(), List.of())));
|
.loadActiveAssigneesGroupedByTaskId(List.of(task.getId())).getOrDefault(task.getId(), List.of())));
|
||||||
respVO.setTotalSpentHours(taskWorklogService.sumDurationByTaskId(task.getId()));
|
respVO.setTotalSpentHours(taskWorklogService.sumDurationByTaskId(task.getId()));
|
||||||
respVO.setExecutionOwnerId(execution.getOwnerId());
|
respVO.setExecutionOwnerId(execution.getOwnerId());
|
||||||
|
respVO.setExecutionName(execution.getExecutionName());
|
||||||
|
respVO.setExecutionStatusCode(execution.getStatusCode());
|
||||||
if (task.getParentTaskId() != null) {
|
if (task.getParentTaskId() != null) {
|
||||||
ProjectTaskDO parentTask = projectTaskMapper.selectById(task.getParentTaskId());
|
ProjectTaskDO parentTask = projectTaskMapper.selectById(task.getParentTaskId());
|
||||||
respVO.setParentTaskOwnerId(parentTask == null ? null : parentTask.getOwnerId());
|
respVO.setParentTaskOwnerId(parentTask == null ? null : parentTask.getOwnerId());
|
||||||
@@ -471,6 +463,8 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||||
|
permission = ProjectTaskConstants.PERMISSION_QUERY)
|
||||||
public PageResult<ProjectTaskRespVO> getTaskRespVOPage(Long projectId, Long executionId, ProjectTaskPageReqVO reqVO) {
|
public PageResult<ProjectTaskRespVO> getTaskRespVOPage(Long projectId, Long executionId, ProjectTaskPageReqVO reqVO) {
|
||||||
PageResult<ProjectTaskDO> pageResult = getTaskPage(projectId, executionId, reqVO);
|
PageResult<ProjectTaskDO> pageResult = getTaskPage(projectId, executionId, reqVO);
|
||||||
return assembleTaskRespVOPage(projectId, executionId, pageResult);
|
return assembleTaskRespVOPage(projectId, executionId, pageResult);
|
||||||
@@ -489,40 +483,15 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
if (list == null || list.isEmpty()) {
|
if (list == null || list.isEmpty()) {
|
||||||
return voPageResult;
|
return voPageResult;
|
||||||
}
|
}
|
||||||
// 批量装配 ownerNickname + assignees,统一收集所有需要的 userId 一次性查 nickname,避免 N+1
|
List<ProjectTaskDO> taskList = pageResult.getList();
|
||||||
Set<Long> taskIds = list.stream().map(ProjectTaskRespVO::getId)
|
|
||||||
.filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
|
|
||||||
Map<Long, List<TaskAssigneeDO>> assigneeMap = taskAssigneeService
|
|
||||||
.loadActiveAssigneesGroupedByTaskId(taskIds);
|
|
||||||
Map<Long, BigDecimal> spentHoursMap = taskWorklogService.sumDurationGroupedByTaskIds(taskIds);
|
|
||||||
Set<Long> userIdsToResolve = list.stream()
|
|
||||||
.map(ProjectTaskRespVO::getOwnerId)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
|
||||||
assigneeMap.values().forEach(items -> items.forEach(a -> userIdsToResolve.add(a.getUserId())));
|
|
||||||
Map<Long, String> nicknameMap = loadOwnerNicknameMap(userIdsToResolve);
|
|
||||||
// 批量查父任务 owner,避免按 list 循环 N+1
|
|
||||||
Set<Long> parentTaskIds = list.stream().map(ProjectTaskRespVO::getParentTaskId)
|
|
||||||
.filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
|
|
||||||
Map<Long, Long> parentTaskOwnerMap = parentTaskIds.isEmpty()
|
|
||||||
? Map.of()
|
|
||||||
: projectTaskMapper.selectBatchIds(parentTaskIds).stream()
|
|
||||||
.collect(Collectors.toMap(ProjectTaskDO::getId, ProjectTaskDO::getOwnerId));
|
|
||||||
// 执行 owner 单条查询,整页共享(URL 路径定 executionId)
|
// 执行 owner 单条查询,整页共享(URL 路径定 executionId)
|
||||||
ProjectExecutionDO execution = projectExecutionMapper.selectByProjectIdAndId(projectId, executionId);
|
ProjectExecutionDO execution = projectExecutionMapper.selectByProjectIdAndId(projectId, executionId);
|
||||||
Long executionOwnerId = execution == null ? null : execution.getOwnerId();
|
Long executionOwnerId = execution == null ? null : execution.getOwnerId();
|
||||||
fillProjectRequirementInfo(list, execution);
|
fillProjectRequirementInfo(list, execution);
|
||||||
|
// 批量回填 ownerNickname / assignees / totalSpentHours / parentTaskOwnerId(与 executionId 无关)
|
||||||
|
enrichTaskRelations(taskList, list);
|
||||||
list.forEach(vo -> {
|
list.forEach(vo -> {
|
||||||
vo.setOwnerNickname(nicknameMap.get(vo.getOwnerId()));
|
|
||||||
List<TaskAssigneeDO> activeList = assigneeMap.getOrDefault(vo.getId(), List.of());
|
|
||||||
vo.setAssignees(activeList.stream()
|
|
||||||
.map(a -> toAssigneeView(a, nicknameMap.get(a.getUserId())))
|
|
||||||
.collect(Collectors.toList()));
|
|
||||||
vo.setTotalSpentHours(spentHoursMap.getOrDefault(vo.getId(), BigDecimal.ZERO));
|
|
||||||
vo.setExecutionOwnerId(executionOwnerId);
|
vo.setExecutionOwnerId(executionOwnerId);
|
||||||
if (vo.getParentTaskId() != null) {
|
|
||||||
vo.setParentTaskOwnerId(parentTaskOwnerMap.get(vo.getParentTaskId()));
|
|
||||||
}
|
|
||||||
// 列表行 cancel/pause/resume/complete 按钮依赖 availableActions,与详情同款装配 lifecycle。
|
// 列表行 cancel/pause/resume/complete 按钮依赖 availableActions,与详情同款装配 lifecycle。
|
||||||
// 单行装配失败做兜底降级(status_model 缺失等脏数据),避免影响整页返回。
|
// 单行装配失败做兜底降级(status_model 缺失等脏数据),避免影响整页返回。
|
||||||
try {
|
try {
|
||||||
@@ -532,9 +501,112 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
vo.getId(), vo.getStatusCode(), e.getMessage());
|
vo.getId(), vo.getStatusCode(), e.getMessage());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
enrichExecutionInfo(voPageResult.getList());
|
||||||
return voPageResult;
|
return voPageResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量回填与 executionId 无关的任务关联字段:ownerNickname / assignees / totalSpentHours / parentTaskOwnerId。
|
||||||
|
* <p>由 {@link #assembleTaskRespVOPage} 与 {@link #assembleTaskRespVOPageCrossExecution} 共享,
|
||||||
|
* 抽取后两条装配路径保证字段口径一致,避免演进漂移。
|
||||||
|
*
|
||||||
|
* @param taskList 原始 DO 列表(用于 enrichTaskRelations 内部如有需要直接访问 DO 字段)
|
||||||
|
* @param voList 已 BeanUtils.toBean 转换好的 VO 列表,本方法在其上 set 字段
|
||||||
|
*/
|
||||||
|
private void enrichTaskRelations(List<ProjectTaskDO> taskList, List<ProjectTaskRespVO> voList) {
|
||||||
|
if (CollUtil.isEmpty(voList)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 批量装配 ownerNickname + assignees,统一收集所有需要的 userId 一次性查 nickname,避免 N+1
|
||||||
|
Set<Long> taskIds = voList.stream().map(ProjectTaskRespVO::getId)
|
||||||
|
.filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
Map<Long, List<TaskAssigneeDO>> assigneeMap = taskAssigneeService
|
||||||
|
.loadActiveAssigneesGroupedByTaskId(taskIds);
|
||||||
|
Map<Long, BigDecimal> spentHoursMap = taskWorklogService.sumDurationGroupedByTaskIds(taskIds);
|
||||||
|
Set<Long> userIdsToResolve = voList.stream()
|
||||||
|
.map(ProjectTaskRespVO::getOwnerId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
assigneeMap.values().forEach(items -> items.forEach(a -> userIdsToResolve.add(a.getUserId())));
|
||||||
|
Map<Long, String> nicknameMap = loadOwnerNicknameMap(userIdsToResolve);
|
||||||
|
// 批量查父任务 owner,避免按 list 循环 N+1
|
||||||
|
Set<Long> parentTaskIds = voList.stream().map(ProjectTaskRespVO::getParentTaskId)
|
||||||
|
.filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
Map<Long, Long> parentTaskOwnerMap;
|
||||||
|
if (parentTaskIds.isEmpty()) {
|
||||||
|
parentTaskOwnerMap = Map.of();
|
||||||
|
} else {
|
||||||
|
parentTaskOwnerMap = new HashMap<>();
|
||||||
|
for (ProjectTaskDO p : projectTaskMapper.selectBatchIds(parentTaskIds)) {
|
||||||
|
parentTaskOwnerMap.put(p.getId(), p.getOwnerId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
voList.forEach(vo -> {
|
||||||
|
vo.setOwnerNickname(nicknameMap.get(vo.getOwnerId()));
|
||||||
|
List<TaskAssigneeDO> activeList = assigneeMap.getOrDefault(vo.getId(), List.of());
|
||||||
|
vo.setAssignees(activeList.stream()
|
||||||
|
.map(a -> toAssigneeView(a, nicknameMap.get(a.getUserId())))
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
vo.setTotalSpentHours(spentHoursMap.getOrDefault(vo.getId(), BigDecimal.ZERO));
|
||||||
|
if (vo.getParentTaskId() != null) {
|
||||||
|
vo.setParentTaskOwnerId(parentTaskOwnerMap.get(vo.getParentTaskId()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResult<ProjectTaskRespVO> assembleTaskRespVOPageCrossExecution(Long projectId, PageResult<ProjectTaskDO> doPage) {
|
||||||
|
if (doPage == null || CollUtil.isEmpty(doPage.getList())) {
|
||||||
|
return new PageResult<>(Collections.emptyList(), doPage == null ? 0L : doPage.getTotal());
|
||||||
|
}
|
||||||
|
List<ProjectTaskDO> taskList = doPage.getList();
|
||||||
|
List<ProjectTaskRespVO> voList = BeanUtils.toBean(taskList, ProjectTaskRespVO.class);
|
||||||
|
|
||||||
|
// 批量回填 ownerNickname / assignees / totalSpentHours / parentTaskOwnerId(与 executionId 无关)
|
||||||
|
enrichTaskRelations(taskList, voList);
|
||||||
|
|
||||||
|
voList.forEach(vo -> {
|
||||||
|
// executionOwnerId 在跨执行场景不设置(涉及多个 execution,无"该执行的 ownerId"概念)
|
||||||
|
try {
|
||||||
|
applyLifecycle(vo);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[assembleTaskRespVOPageCrossExecution] applyLifecycle 装配失败 taskId={}", vo.getId(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按 task.executionId 分组批量回填 executionName / executionStatusCode
|
||||||
|
enrichExecutionInfo(voList);
|
||||||
|
|
||||||
|
return new PageResult<>(voList, doPage.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给一批 ProjectTaskRespVO 批量回填 executionName / executionStatusCode。
|
||||||
|
* 一次查询所有涉及的 executionId,避免 N+1。
|
||||||
|
*/
|
||||||
|
private void enrichExecutionInfo(List<ProjectTaskRespVO> list) {
|
||||||
|
if (CollUtil.isEmpty(list)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<Long> executionIds = list.stream()
|
||||||
|
.map(ProjectTaskRespVO::getExecutionId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
if (executionIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<ProjectExecutionDO> executions = projectExecutionMapper.selectBatchIds(executionIds);
|
||||||
|
Map<Long, ProjectExecutionDO> executionMap = executions.stream()
|
||||||
|
.collect(Collectors.toMap(ProjectExecutionDO::getId, e -> e, (a, b) -> a));
|
||||||
|
list.forEach(vo -> {
|
||||||
|
ProjectExecutionDO exec = executionMap.get(vo.getExecutionId());
|
||||||
|
if (exec != null) {
|
||||||
|
vo.setExecutionName(exec.getExecutionName());
|
||||||
|
vo.setExecutionStatusCode(exec.getStatusCode());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 把任务 RespVO 上的项目需求信息(TD-013)回填。
|
* 把任务 RespVO 上的项目需求信息(TD-013)回填。
|
||||||
* <p>本服务的 task 查询入口均以 executionId 为收口(详情 / page / board-page 共用 assembleTaskRespVOPage),
|
* <p>本服务的 task 查询入口均以 executionId 为收口(详情 / page / board-page 共用 assembleTaskRespVOPage),
|
||||||
@@ -628,7 +700,7 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
* - 进入终态(toStatus.terminalFlag=true)且未填写时,写入 actualEndDate
|
* - 进入终态(toStatus.terminalFlag=true)且未填写时,写入 actualEndDate
|
||||||
*/
|
*/
|
||||||
private void maybeFillActualDates(ProjectTaskDO task, String fromStatus, String toStatus) {
|
private void maybeFillActualDates(ProjectTaskDO task, String fromStatus, String toStatus) {
|
||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now(SERVER_ZONE);
|
||||||
LocalDate newActualStart = null;
|
LocalDate newActualStart = null;
|
||||||
LocalDate newActualEnd = null;
|
LocalDate newActualEnd = null;
|
||||||
if (task.getActualStartDate() == null) {
|
if (task.getActualStartDate() == null) {
|
||||||
@@ -810,11 +882,16 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
writeTaskAuditLog(task, actionCode, fromStatus, toStatus, null, reason);
|
writeTaskAuditLog(task, actionCode, fromStatus, toStatus, null, reason);
|
||||||
maybeFillActualDates(task, fromStatus, toStatus);
|
maybeFillActualDates(task, fromStatus, toStatus);
|
||||||
|
|
||||||
// 完成动作:兜底把任务进度刷到 100%,并触发父任务 AVG 重算
|
// 完成动作:兜底把任务进度刷到 100%,并触发父任务 AVG 重算(forceCompleteProgress 内部触发项目刷新)
|
||||||
if ("complete".equals(actionCode)) {
|
if ("complete".equals(actionCode)) {
|
||||||
forceCompleteProgress(task);
|
forceCompleteProgress(task);
|
||||||
} else if (task.getParentTaskId() != null) {
|
} else {
|
||||||
recalcParentProgressFrom(task.getParentTaskId());
|
if (task.getParentTaskId() != null) {
|
||||||
|
recalcParentProgressFrom(task.getParentTaskId());
|
||||||
|
}
|
||||||
|
// 项目进度推算:非完成状态变更(cancel/pause/resume)。cancel 进 progress_excluded 影响项目均值;
|
||||||
|
// pause/resume 不影响 progress_excluded,但根任务的 pause/resume 不改 progressRate 时 recalc 也是幂等的
|
||||||
|
projectService.recalcProgress(task.getProjectId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消 / 暂停 / 恢复:触发子任务级联(每个子任务的内部链路自身会再级联自己的子,链式实现整棵子树)
|
// 取消 / 暂停 / 恢复:触发子任务级联(每个子任务的内部链路自身会再级联自己的子,链式实现整棵子树)
|
||||||
@@ -834,6 +911,7 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
private void forceCompleteProgress(ProjectTaskDO task) {
|
private void forceCompleteProgress(ProjectTaskDO task) {
|
||||||
BigDecimal full = BigDecimal.valueOf(100);
|
BigDecimal full = BigDecimal.valueOf(100);
|
||||||
if (progressNumericallyEquals(task.getProgressRate(), full)) {
|
if (progressNumericallyEquals(task.getProgressRate(), full)) {
|
||||||
|
// 进度已经是 100:completed 不在 progress_excluded 集合内,对项目均值无影响,跳过刷新
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
projectTaskMapper.updateProgressRateById(task.getId(), full);
|
projectTaskMapper.updateProgressRateById(task.getId(), full);
|
||||||
@@ -841,6 +919,8 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
|||||||
if (task.getParentTaskId() != null) {
|
if (task.getParentTaskId() != null) {
|
||||||
recalcParentProgressFrom(task.getParentTaskId());
|
recalcParentProgressFrom(task.getParentTaskId());
|
||||||
}
|
}
|
||||||
|
// 项目进度推算:根任务 complete 时父链不冒泡,但任务自身 progressRate=100 已变化,必须触发刷新
|
||||||
|
projectService.recalcProgress(task.getProjectId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.njcn.rdms.module.project.util;
|
||||||
|
|
||||||
|
import java.time.DayOfWeek;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.temporal.TemporalAdjusters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dueRange 截止时间范围筛选的日期边界 helper。
|
||||||
|
*
|
||||||
|
* <p>口径统一:服务器时区 {@code Asia/Shanghai},本周按周一~周日。
|
||||||
|
* 供执行分页查询与执行状态看板计数共用,避免在多个 service 里重复同一段日期计算。</p>
|
||||||
|
*
|
||||||
|
* <p>终态排除不在此处:终态码由各对象域自行通过
|
||||||
|
* {@code ObjectStatusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled(objectType)} 动态查。</p>
|
||||||
|
*/
|
||||||
|
public final class DueRangeSupport {
|
||||||
|
|
||||||
|
private static final ZoneId SERVER_ZONE = ZoneId.of("Asia/Shanghai");
|
||||||
|
|
||||||
|
private DueRangeSupport() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 服务器当天。 */
|
||||||
|
public static LocalDate today() {
|
||||||
|
return LocalDate.now(SERVER_ZONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 本周一(含当天)。 */
|
||||||
|
public static LocalDate weekStart(LocalDate today) {
|
||||||
|
return today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 本周日(含当天)。 */
|
||||||
|
public static LocalDate weekEnd(LocalDate today) {
|
||||||
|
return today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,7 +56,7 @@ spring:
|
|||||||
primary: master
|
primary: master
|
||||||
datasource:
|
datasource:
|
||||||
master:
|
master:
|
||||||
url: jdbc:mysql://192.168.1.22:13306/rdms_v3?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
url: jdbc:mysql://192.168.1.22:13306/rdms_view?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
||||||
username: root
|
username: root
|
||||||
password: njcnpqs
|
password: njcnpqs
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ spring:
|
|||||||
primary: master
|
primary: master
|
||||||
datasource:
|
datasource:
|
||||||
master:
|
master:
|
||||||
url: jdbc:mysql://192.168.1.22:13306/rdms_v3?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
url: jdbc:mysql://192.168.1.22:13306/rdms_view?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
||||||
username: root
|
username: root
|
||||||
password: njcnpqs
|
password: njcnpqs
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
|
|||||||
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
|
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
|
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
|
||||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
|
||||||
import com.njcn.rdms.module.project.service.project.permission.VisibilityScope;
|
|
||||||
import com.njcn.rdms.module.project.service.project.permission.VisibilityScopeResolver;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
@@ -24,7 +21,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.lenient;
|
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
class ProjectStatusBoardServiceTest extends BaseMockitoUnitTest {
|
class ProjectStatusBoardServiceTest extends BaseMockitoUnitTest {
|
||||||
@@ -37,17 +33,6 @@ class ProjectStatusBoardServiceTest extends BaseMockitoUnitTest {
|
|||||||
private ProjectExecutionMapper projectExecutionMapper;
|
private ProjectExecutionMapper projectExecutionMapper;
|
||||||
@Mock
|
@Mock
|
||||||
private ProjectTaskMapper projectTaskMapper;
|
private ProjectTaskMapper projectTaskMapper;
|
||||||
@Mock
|
|
||||||
private VisibilityScopeResolver visibilityScopeResolver;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认让 VisibilityScopeResolver 放行(seesAll=true),既有看板用例不关心 scope。
|
|
||||||
*/
|
|
||||||
@BeforeEach
|
|
||||||
void setupVisibilityScopeAll() {
|
|
||||||
lenient().when(visibilityScopeResolver.resolveForProject(any(), any())).thenReturn(VisibilityScope.all());
|
|
||||||
lenient().when(visibilityScopeResolver.resolveForExecution(any(), any(), any())).thenReturn(VisibilityScope.all());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getExecutionStatusBoard_shouldReturnEnabledStatusesInSortOrderAndSumCounts() {
|
void getExecutionStatusBoard_shouldReturnEnabledStatusesInSortOrderAndSumCounts() {
|
||||||
@@ -59,16 +44,21 @@ class ProjectStatusBoardServiceTest extends BaseMockitoUnitTest {
|
|||||||
createStatus("cancelled", "已取消", 50, true),
|
createStatus("cancelled", "已取消", 50, true),
|
||||||
createStatus("disabled", "已停用", 60, false, 1)
|
createStatus("disabled", "已停用", 60, false, 1)
|
||||||
));
|
));
|
||||||
when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L), any(VisibilityScope.class),
|
when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L),
|
||||||
any(ProjectExecutionStatusBoardReqVO.class), eq("pending"))).thenReturn(3);
|
any(ProjectExecutionStatusBoardReqVO.class), eq("pending"),
|
||||||
when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L), any(VisibilityScope.class),
|
any(), any(), any(), any())).thenReturn(3);
|
||||||
any(ProjectExecutionStatusBoardReqVO.class), eq("active"))).thenReturn(8);
|
when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L),
|
||||||
when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L), any(VisibilityScope.class),
|
any(ProjectExecutionStatusBoardReqVO.class), eq("active"),
|
||||||
any(ProjectExecutionStatusBoardReqVO.class), eq("paused"))).thenReturn(2);
|
any(), any(), any(), any())).thenReturn(8);
|
||||||
when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L), any(VisibilityScope.class),
|
when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L),
|
||||||
any(ProjectExecutionStatusBoardReqVO.class), eq("completed"))).thenReturn(4);
|
any(ProjectExecutionStatusBoardReqVO.class), eq("paused"),
|
||||||
when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L), any(VisibilityScope.class),
|
any(), any(), any(), any())).thenReturn(2);
|
||||||
any(ProjectExecutionStatusBoardReqVO.class), eq("cancelled"))).thenReturn(1);
|
when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L),
|
||||||
|
any(ProjectExecutionStatusBoardReqVO.class), eq("completed"),
|
||||||
|
any(), any(), any(), any())).thenReturn(4);
|
||||||
|
when(projectExecutionMapper.countByProjectIdAndStatusCode(eq(2001L),
|
||||||
|
any(ProjectExecutionStatusBoardReqVO.class), eq("cancelled"),
|
||||||
|
any(), any(), any(), any())).thenReturn(1);
|
||||||
|
|
||||||
ProjectExecutionStatusBoardReqVO reqVO = new ProjectExecutionStatusBoardReqVO();
|
ProjectExecutionStatusBoardReqVO reqVO = new ProjectExecutionStatusBoardReqVO();
|
||||||
reqVO.setKeyword("接口");
|
reqVO.setKeyword("接口");
|
||||||
@@ -100,15 +90,15 @@ class ProjectStatusBoardServiceTest extends BaseMockitoUnitTest {
|
|||||||
createStatus("cancelled", "已取消", 50, true)
|
createStatus("cancelled", "已取消", 50, true)
|
||||||
));
|
));
|
||||||
when(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(eq(2001L), eq(5001L),
|
when(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(eq(2001L), eq(5001L),
|
||||||
any(VisibilityScope.class), any(ProjectTaskStatusBoardReqVO.class), eq("pending"))).thenReturn(5);
|
any(ProjectTaskStatusBoardReqVO.class), eq("pending"))).thenReturn(5);
|
||||||
when(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(eq(2001L), eq(5001L),
|
when(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(eq(2001L), eq(5001L),
|
||||||
any(VisibilityScope.class), any(ProjectTaskStatusBoardReqVO.class), eq("active"))).thenReturn(12);
|
any(ProjectTaskStatusBoardReqVO.class), eq("active"))).thenReturn(12);
|
||||||
when(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(eq(2001L), eq(5001L),
|
when(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(eq(2001L), eq(5001L),
|
||||||
any(VisibilityScope.class), any(ProjectTaskStatusBoardReqVO.class), eq("paused"))).thenReturn(2);
|
any(ProjectTaskStatusBoardReqVO.class), eq("paused"))).thenReturn(2);
|
||||||
when(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(eq(2001L), eq(5001L),
|
when(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(eq(2001L), eq(5001L),
|
||||||
any(VisibilityScope.class), any(ProjectTaskStatusBoardReqVO.class), eq("completed"))).thenReturn(4);
|
any(ProjectTaskStatusBoardReqVO.class), eq("completed"))).thenReturn(4);
|
||||||
when(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(eq(2001L), eq(5001L),
|
when(projectTaskMapper.countByProjectIdAndExecutionIdAndStatusCode(eq(2001L), eq(5001L),
|
||||||
any(VisibilityScope.class), any(ProjectTaskStatusBoardReqVO.class), eq("cancelled"))).thenReturn(1);
|
any(ProjectTaskStatusBoardReqVO.class), eq("cancelled"))).thenReturn(1);
|
||||||
|
|
||||||
ProjectTaskStatusBoardReqVO reqVO = new ProjectTaskStatusBoardReqVO();
|
ProjectTaskStatusBoardReqVO reqVO = new ProjectTaskStatusBoardReqVO();
|
||||||
reqVO.setKeyword("任务");
|
reqVO.setKeyword("任务");
|
||||||
|
|||||||
@@ -98,23 +98,16 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private ProjectExecutionAssigneeService projectExecutionAssigneeService;
|
private ProjectExecutionAssigneeService projectExecutionAssigneeService;
|
||||||
@Mock
|
@Mock
|
||||||
private com.njcn.rdms.module.project.service.project.permission.VisibilityScopeResolver visibilityScopeResolver;
|
|
||||||
@Mock
|
|
||||||
private ProjectRequirementService projectRequirementService;
|
private ProjectRequirementService projectRequirementService;
|
||||||
@Mock
|
@Mock
|
||||||
private ProjectRequirementMapper projectRequirementMapper;
|
private ProjectRequirementMapper projectRequirementMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认让 VisibilityScopeResolver 放行(seesAll=true),既有测试无需关心 scope。
|
|
||||||
* 默认让 dictDataApi.validateDictDataList 对 REQ_PRIORITY 返回 true,既有测试不因 priority 校验失败。
|
* 默认让 dictDataApi.validateDictDataList 对 REQ_PRIORITY 返回 true,既有测试不因 priority 校验失败。
|
||||||
* 真正需要测试 scope 行为的用例可在方法内显式覆盖。
|
* 读路径鉴权由 @CheckObjectPermission 的 AOP 处理,单测 @InjectMocks 不走 AOP,无须在此 mock。
|
||||||
*/
|
*/
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setupVisibilityScopeAll() {
|
void setupDefaultPriorityValidation() {
|
||||||
lenient().when(visibilityScopeResolver.resolveForProject(any(), any()))
|
|
||||||
.thenReturn(com.njcn.rdms.module.project.service.project.permission.VisibilityScope.all());
|
|
||||||
lenient().when(visibilityScopeResolver.resolveForExecution(any(), any(), any()))
|
|
||||||
.thenReturn(com.njcn.rdms.module.project.service.project.permission.VisibilityScope.all());
|
|
||||||
lenient().when(dictDataApi.validateDictDataList(eq(ProjectDictTypeConstants.REQ_PRIORITY), any()))
|
lenient().when(dictDataApi.validateDictDataList(eq(ProjectDictTypeConstants.REQ_PRIORITY), any()))
|
||||||
.thenReturn(success(true));
|
.thenReturn(success(true));
|
||||||
}
|
}
|
||||||
@@ -557,7 +550,7 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
|||||||
second.setProgressRate(new BigDecimal("100.00"));
|
second.setProgressRate(new BigDecimal("100.00"));
|
||||||
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
||||||
when(projectExecutionMapper.selectPageByProjectId(eq(projectId), any(), eq(reqVO)))
|
when(projectExecutionMapper.selectPageByProjectId(eq(projectId), eq(reqVO), any(), any(), any(), any()))
|
||||||
.thenReturn(new PageResult<>(List.of(first, second), 2L));
|
.thenReturn(new PageResult<>(List.of(first, second), 2L));
|
||||||
when(objectStatusModelMapper.selectProgressExcludedStatusCodesByObjectTypeEnabled("task"))
|
when(objectStatusModelMapper.selectProgressExcludedStatusCodesByObjectTypeEnabled("task"))
|
||||||
.thenReturn(List.of("cancelled"));
|
.thenReturn(List.of("cancelled"));
|
||||||
@@ -593,7 +586,7 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
|||||||
rows.add(Map.of("executionId", 5002L, "progressRate", 10));
|
rows.add(Map.of("executionId", 5002L, "progressRate", 10));
|
||||||
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
||||||
when(projectExecutionMapper.selectPageByProjectId(eq(projectId), any(), eq(reqVO)))
|
when(projectExecutionMapper.selectPageByProjectId(eq(projectId), eq(reqVO), any(), any(), any(), any()))
|
||||||
.thenReturn(new PageResult<>(List.of(first, second), 2L));
|
.thenReturn(new PageResult<>(List.of(first, second), 2L));
|
||||||
when(objectStatusModelMapper.selectProgressExcludedStatusCodesByObjectTypeEnabled("task"))
|
when(objectStatusModelMapper.selectProgressExcludedStatusCodesByObjectTypeEnabled("task"))
|
||||||
.thenReturn(List.of("cancelled"));
|
.thenReturn(List.of("cancelled"));
|
||||||
@@ -654,7 +647,7 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
|||||||
reqVO.setPageNo(1);
|
reqVO.setPageNo(1);
|
||||||
reqVO.setPageSize(20);
|
reqVO.setPageSize(20);
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
||||||
when(projectExecutionMapper.selectPageByProjectId(eq(projectId), any(), eq(reqVO)))
|
when(projectExecutionMapper.selectPageByProjectId(eq(projectId), eq(reqVO), any(), any(), any(), any()))
|
||||||
.thenReturn(new PageResult<>(List.of(), 0L));
|
.thenReturn(new PageResult<>(List.of(), 0L));
|
||||||
|
|
||||||
PageResult<ProjectExecutionDO> result = projectExecutionService.getExecutionPage(projectId, reqVO);
|
PageResult<ProjectExecutionDO> result = projectExecutionService.getExecutionPage(projectId, reqVO);
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
package com.njcn.rdms.module.project.service.project.permission;
|
|
||||||
|
|
||||||
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
|
|
||||||
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.execution.ExecutionAssigneeMapper;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
|
|
||||||
import com.njcn.rdms.module.project.dal.mysql.project.task.TaskAssigneeMapper;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
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.Mockito.when;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* VisibilityScopeResolverImpl 单元测试。覆盖角色矩阵:
|
|
||||||
* - 项目经理 → seesAll
|
|
||||||
* - 非项目经理 → 4 源并集
|
|
||||||
* - 非项目经理且无任何参与 → 空集合
|
|
||||||
* - 执行维度同上
|
|
||||||
*/
|
|
||||||
class VisibilityScopeResolverImplTest extends BaseMockitoUnitTest {
|
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
private VisibilityScopeResolverImpl resolver;
|
|
||||||
|
|
||||||
@Mock private ProjectMapper projectMapper;
|
|
||||||
@Mock private ProjectExecutionMapper projectExecutionMapper;
|
|
||||||
@Mock private ExecutionAssigneeMapper executionAssigneeMapper;
|
|
||||||
@Mock private ProjectTaskMapper projectTaskMapper;
|
|
||||||
@Mock private TaskAssigneeMapper taskAssigneeMapper;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void resolveForProject_managerShouldSeeAll() {
|
|
||||||
Long projectId = 2001L, userId = 3001L;
|
|
||||||
ProjectDO project = new ProjectDO();
|
|
||||||
project.setId(projectId);
|
|
||||||
project.setManagerUserId(userId);
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(project);
|
|
||||||
|
|
||||||
VisibilityScope scope = resolver.resolveForProject(projectId, userId);
|
|
||||||
|
|
||||||
assertTrue(scope.seesAll());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void resolveForProject_nonManagerUnionsFourSources() {
|
|
||||||
Long projectId = 2001L, userId = 3002L;
|
|
||||||
ProjectDO project = new ProjectDO();
|
|
||||||
project.setId(projectId);
|
|
||||||
project.setManagerUserId(9999L);
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(project);
|
|
||||||
|
|
||||||
when(projectExecutionMapper.selectIdsByProjectIdAndOwnerId(projectId, userId))
|
|
||||||
.thenReturn(List.of(5001L));
|
|
||||||
when(executionAssigneeMapper.selectActiveExecutionIdsByProjectIdAndUserId(projectId, userId))
|
|
||||||
.thenReturn(List.of(5002L));
|
|
||||||
when(projectTaskMapper.selectOwnedTaskAndDescendantIdsByProjectIdAndUserId(projectId, userId))
|
|
||||||
.thenReturn(List.of(9001L, 9002L));
|
|
||||||
when(taskAssigneeMapper.selectActiveTaskIdsByProjectIdAndUserId(projectId, userId))
|
|
||||||
.thenReturn(List.of(9003L));
|
|
||||||
|
|
||||||
VisibilityScope scope = resolver.resolveForProject(projectId, userId);
|
|
||||||
|
|
||||||
assertFalse(scope.seesAll());
|
|
||||||
assertEquals(Set.of(5001L, 5002L), scope.executionIds());
|
|
||||||
assertEquals(Set.of(9001L, 9002L, 9003L), scope.taskIds());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void resolveForProject_nonParticipantReturnsEmpty() {
|
|
||||||
Long projectId = 2001L, userId = 3099L;
|
|
||||||
ProjectDO project = new ProjectDO();
|
|
||||||
project.setId(projectId);
|
|
||||||
project.setManagerUserId(9999L);
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(project);
|
|
||||||
when(projectExecutionMapper.selectIdsByProjectIdAndOwnerId(projectId, userId)).thenReturn(List.of());
|
|
||||||
when(executionAssigneeMapper.selectActiveExecutionIdsByProjectIdAndUserId(projectId, userId)).thenReturn(List.of());
|
|
||||||
when(projectTaskMapper.selectOwnedTaskAndDescendantIdsByProjectIdAndUserId(projectId, userId)).thenReturn(List.of());
|
|
||||||
when(taskAssigneeMapper.selectActiveTaskIdsByProjectIdAndUserId(projectId, userId)).thenReturn(List.of());
|
|
||||||
|
|
||||||
VisibilityScope scope = resolver.resolveForProject(projectId, userId);
|
|
||||||
|
|
||||||
assertFalse(scope.seesAll());
|
|
||||||
assertTrue(scope.executionIds().isEmpty());
|
|
||||||
assertTrue(scope.taskIds().isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void resolveForExecution_managerShouldSeeAll() {
|
|
||||||
Long projectId = 2001L, executionId = 5001L, userId = 3001L;
|
|
||||||
ProjectDO project = new ProjectDO();
|
|
||||||
project.setManagerUserId(userId);
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(project);
|
|
||||||
|
|
||||||
VisibilityScope scope = resolver.resolveForExecution(projectId, executionId, userId);
|
|
||||||
|
|
||||||
assertTrue(scope.seesAll());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void resolveForExecution_nonManagerScopedToThatExecution() {
|
|
||||||
Long projectId = 2001L, executionId = 5001L, userId = 3002L;
|
|
||||||
ProjectDO project = new ProjectDO();
|
|
||||||
project.setManagerUserId(9999L);
|
|
||||||
when(projectMapper.selectById(projectId)).thenReturn(project);
|
|
||||||
|
|
||||||
when(projectTaskMapper.selectOwnedTaskAndDescendantIdsByExecutionIdAndUserId(projectId, executionId, userId))
|
|
||||||
.thenReturn(List.of(9001L));
|
|
||||||
when(taskAssigneeMapper.selectActiveTaskIdsByProjectIdAndExecutionIdAndUserId(projectId, executionId, userId))
|
|
||||||
.thenReturn(List.of(9002L));
|
|
||||||
|
|
||||||
VisibilityScope scope = resolver.resolveForExecution(projectId, executionId, userId);
|
|
||||||
|
|
||||||
assertFalse(scope.seesAll());
|
|
||||||
assertTrue(scope.executionIds().isEmpty());
|
|
||||||
assertEquals(Set.of(9001L, 9002L), scope.taskIds());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
|
|||||||
import cn.hutool.core.map.MapUtil;
|
import cn.hutool.core.map.MapUtil;
|
||||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||||
import com.njcn.rdms.framework.common.util.collection.CollectionUtils;
|
import com.njcn.rdms.framework.common.util.collection.CollectionUtils;
|
||||||
|
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||||
import com.njcn.rdms.module.system.api.user.dto.UserManagementRelationRespDTO;
|
import com.njcn.rdms.module.system.api.user.dto.UserManagementRelationRespDTO;
|
||||||
import com.njcn.rdms.module.system.enums.ApiConstants;
|
import com.njcn.rdms.module.system.enums.ApiConstants;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@@ -33,6 +34,11 @@ public interface UserManagementRelationApi {
|
|||||||
@Parameter(name = "subordinateUserId", description = "被管理者用户ID", example = "2", required = true)
|
@Parameter(name = "subordinateUserId", description = "被管理者用户ID", example = "2", required = true)
|
||||||
CommonResult<List<UserManagementRelationRespDTO>> getRelationListBySubordinateUserId(@RequestParam("subordinateUserId") Long subordinateUserId);
|
CommonResult<List<UserManagementRelationRespDTO>> getRelationListBySubordinateUserId(@RequestParam("subordinateUserId") Long subordinateUserId);
|
||||||
|
|
||||||
|
@GetMapping(PREFIX + "/direct-manager")
|
||||||
|
@Operation(summary = "根据用户ID获得当前生效的直属上级")
|
||||||
|
@Parameter(name = "userId", description = "用户ID", example = "2", required = true)
|
||||||
|
CommonResult<AdminUserRespDTO> getDirectManager(@RequestParam("userId") Long userId);
|
||||||
|
|
||||||
@GetMapping(PREFIX + "/list")
|
@GetMapping(PREFIX + "/list")
|
||||||
@Operation(summary = "获得管理链路列表")
|
@Operation(summary = "获得管理链路列表")
|
||||||
@Parameter(name = "ids", description = "关系编号数组", example = "1,2", required = true)
|
@Parameter(name = "ids", description = "关系编号数组", example = "1,2", required = true)
|
||||||
|
|||||||
@@ -32,4 +32,14 @@ public interface DictTypeConstants {
|
|||||||
*/
|
*/
|
||||||
String RDMS_TASK_ITEM_TYPE="rdms_task_item_type";
|
String RDMS_TASK_ITEM_TYPE="rdms_task_item_type";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加班申请审批状态字典。
|
||||||
|
*/
|
||||||
|
String RDMS_OVERTIME_APPLICATION_STATUS = "rdms_overtime_application_status";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加班申请时长快捷选项字典。
|
||||||
|
*/
|
||||||
|
String RDMS_OVERTIME_DURATION = "rdms_overtime_duration";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package com.njcn.rdms.module.system.api.user;
|
|||||||
|
|
||||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||||
|
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||||
import com.njcn.rdms.module.system.api.user.dto.UserManagementRelationRespDTO;
|
import com.njcn.rdms.module.system.api.user.dto.UserManagementRelationRespDTO;
|
||||||
|
import com.njcn.rdms.module.system.dal.dataobject.user.AdminUserDO;
|
||||||
import com.njcn.rdms.module.system.dal.dataobject.user.UserManagementRelationDO;
|
import com.njcn.rdms.module.system.dal.dataobject.user.UserManagementRelationDO;
|
||||||
import com.njcn.rdms.module.system.service.user.UserManagementRelationService;
|
import com.njcn.rdms.module.system.service.user.UserManagementRelationService;
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
@@ -36,6 +38,15 @@ public class UserManagementRelationApiImpl implements UserManagementRelationApi
|
|||||||
return success(BeanUtils.toBean(list, UserManagementRelationRespDTO.class));
|
return success(BeanUtils.toBean(list, UserManagementRelationRespDTO.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommonResult<AdminUserRespDTO> getDirectManager(Long userId) {
|
||||||
|
AdminUserDO manager = userManagementRelationService.getDirectManager(userId);
|
||||||
|
if (manager == null) {
|
||||||
|
return success(null);
|
||||||
|
}
|
||||||
|
return success(BeanUtils.toBean(manager, AdminUserRespDTO.class));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommonResult<List<UserManagementRelationRespDTO>> getRelationList(Collection<Long> ids) {
|
public CommonResult<List<UserManagementRelationRespDTO>> getRelationList(Collection<Long> ids) {
|
||||||
if (ids == null || ids.isEmpty()) {
|
if (ids == null || ids.isEmpty()) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import org.springframework.validation.annotation.Validated;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -157,6 +158,26 @@ public class UserManagementRelationController {
|
|||||||
return success(list);
|
return success(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得某用户当前生效的直属上级
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @return 直属上级用户,不存在则返回 null
|
||||||
|
*/
|
||||||
|
@GetMapping("/direct-manager")
|
||||||
|
@Operation(summary = "获得某用户当前生效的直属上级")
|
||||||
|
@Parameter(name = "userId", description = "用户ID", required = true, example = "1024")
|
||||||
|
@PreAuthorize("@ss.hasPermission('system:user-management-relation:query')")
|
||||||
|
public CommonResult<UserSimpleRespVO> getDirectManager(@RequestParam("userId") Long userId) {
|
||||||
|
AdminUserDO manager = userManagementRelationService.getDirectManager(userId);
|
||||||
|
if (manager == null) {
|
||||||
|
return success(null);
|
||||||
|
}
|
||||||
|
List<Long> deptIds = manager.getDeptId() == null ? Collections.emptyList() : List.of(manager.getDeptId());
|
||||||
|
Map<Long, DeptDO> deptMap = deptService.getDeptMap(deptIds);
|
||||||
|
return success(UserConvert.INSTANCE.convertSimpleList(List.of(manager), deptMap).get(0));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取未绑定直属上级的候选下级用户列表
|
* 获取未绑定直属上级的候选下级用户列表
|
||||||
* @return 候选下级用户列表
|
* @return 候选下级用户列表
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.njcn.rdms.framework.encrypt.core.annotation.ApiEncrypt;
|
|||||||
import com.njcn.rdms.module.system.controller.admin.user.vo.profile.UserProfileRespVO;
|
import com.njcn.rdms.module.system.controller.admin.user.vo.profile.UserProfileRespVO;
|
||||||
import com.njcn.rdms.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO;
|
import com.njcn.rdms.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO;
|
||||||
import com.njcn.rdms.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO;
|
import com.njcn.rdms.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO;
|
||||||
|
import com.njcn.rdms.module.system.controller.admin.user.vo.user.UserSimpleRespVO;
|
||||||
import com.njcn.rdms.module.system.convert.user.UserConvert;
|
import com.njcn.rdms.module.system.convert.user.UserConvert;
|
||||||
import com.njcn.rdms.module.system.dal.dataobject.dept.DeptDO;
|
import com.njcn.rdms.module.system.dal.dataobject.dept.DeptDO;
|
||||||
import com.njcn.rdms.module.system.dal.dataobject.dept.PostDO;
|
import com.njcn.rdms.module.system.dal.dataobject.dept.PostDO;
|
||||||
@@ -17,6 +18,7 @@ import com.njcn.rdms.module.system.service.dept.PostService;
|
|||||||
import com.njcn.rdms.module.system.service.permission.PermissionService;
|
import com.njcn.rdms.module.system.service.permission.PermissionService;
|
||||||
import com.njcn.rdms.module.system.service.permission.RoleService;
|
import com.njcn.rdms.module.system.service.permission.RoleService;
|
||||||
import com.njcn.rdms.module.system.service.user.AdminUserService;
|
import com.njcn.rdms.module.system.service.user.AdminUserService;
|
||||||
|
import com.njcn.rdms.module.system.service.user.UserManagementRelationService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@@ -26,7 +28,9 @@ import org.springframework.validation.annotation.Validated;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||||
import static com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
import static com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
||||||
@@ -48,6 +52,8 @@ public class UserProfileController {
|
|||||||
private PermissionService permissionService;
|
private PermissionService permissionService;
|
||||||
@Resource
|
@Resource
|
||||||
private RoleService roleService;
|
private RoleService roleService;
|
||||||
|
@Resource
|
||||||
|
private UserManagementRelationService userManagementRelationService;
|
||||||
|
|
||||||
@GetMapping("/get")
|
@GetMapping("/get")
|
||||||
@Operation(summary = "获得登录用户信息")
|
@Operation(summary = "获得登录用户信息")
|
||||||
@@ -67,6 +73,18 @@ public class UserProfileController {
|
|||||||
return success(UserConvert.INSTANCE.convert(user, userRoles, dept, position));
|
return success(UserConvert.INSTANCE.convert(user, userRoles, dept, position));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/direct-manager")
|
||||||
|
@Operation(summary = "获得当前登录用户的直属上级")
|
||||||
|
public CommonResult<UserSimpleRespVO> getLoginUserDirectManager() {
|
||||||
|
AdminUserDO manager = userManagementRelationService.getDirectManager(getLoginUserId());
|
||||||
|
if (manager == null) {
|
||||||
|
return success(null);
|
||||||
|
}
|
||||||
|
List<Long> deptIds = manager.getDeptId() == null ? Collections.emptyList() : List.of(manager.getDeptId());
|
||||||
|
Map<Long, DeptDO> deptMap = deptService.getDeptMap(deptIds);
|
||||||
|
return success(UserConvert.INSTANCE.convertSimpleList(List.of(manager), deptMap).get(0));
|
||||||
|
}
|
||||||
|
|
||||||
@PutMapping("/update")
|
@PutMapping("/update")
|
||||||
@Operation(summary = "修改用户个人信息")
|
@Operation(summary = "修改用户个人信息")
|
||||||
public CommonResult<Boolean> updateUserProfile(@Valid @RequestBody UserProfileUpdateReqVO reqVO) {
|
public CommonResult<Boolean> updateUserProfile(@Valid @RequestBody UserProfileUpdateReqVO reqVO) {
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ public class DictDataDO extends BaseDO {
|
|||||||
*
|
*
|
||||||
* 对应到 element-ui 为 default、primary、success、info、warning、danger
|
* 对应到 element-ui 为 default、primary、success、info、warning、danger
|
||||||
*/
|
*/
|
||||||
|
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||||
private String colorType;
|
private String colorType;
|
||||||
/**
|
/**
|
||||||
* css 样式
|
* css 样式
|
||||||
|
|||||||
@@ -73,4 +73,23 @@ public interface UserManagementRelationMapper extends BaseMapperX<UserManagement
|
|||||||
return selectList(UserManagementRelationDO::getSubordinateUserId, subordinateUserId);
|
return selectList(UserManagementRelationDO::getSubordinateUserId, subordinateUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据被管理者用户ID查询当前生效的上级关系列表
|
||||||
|
*
|
||||||
|
* @param subordinateUserId 被管理者用户ID
|
||||||
|
* @return 当前生效的用户管理链路DO列表
|
||||||
|
*/
|
||||||
|
default List<UserManagementRelationDO> selectValidListBySubordinateUserId(Long subordinateUserId) {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
return selectList(new LambdaQueryWrapperX<UserManagementRelationDO>()
|
||||||
|
.eq(UserManagementRelationDO::getSubordinateUserId, subordinateUserId)
|
||||||
|
// (from IS NULL OR from <= now)
|
||||||
|
.and(w -> w.isNull(UserManagementRelationDO::getEffectiveFrom)
|
||||||
|
.or().le(UserManagementRelationDO::getEffectiveFrom, now))
|
||||||
|
// (until IS NULL OR until >= now)
|
||||||
|
.and(w -> w.isNull(UserManagementRelationDO::getEffectiveUntil)
|
||||||
|
.or().ge(UserManagementRelationDO::getEffectiveUntil, now))
|
||||||
|
.orderByDesc(UserManagementRelationDO::getId));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,14 @@ public interface UserManagementRelationService {
|
|||||||
*/
|
*/
|
||||||
List<UserManagementRelationDO> getRelationListBySubordinateUserId(Long subordinateUserId);
|
List<UserManagementRelationDO> getRelationListBySubordinateUserId(Long subordinateUserId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得某用户当前生效的直属上级
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @return 直属上级用户,不存在则返回 null
|
||||||
|
*/
|
||||||
|
AdminUserDO getDirectManager(Long userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获得用户管理链路树形结构
|
* 获得用户管理链路树形结构
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -270,7 +270,33 @@ public class UserManagementRelationServiceImpl implements UserManagementRelation
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<UserManagementRelationDO> getRelationListBySubordinateUserId(Long subordinateUserId) {
|
public List<UserManagementRelationDO> getRelationListBySubordinateUserId(Long subordinateUserId) {
|
||||||
return userManagementRelationMapper.selectListBySubordinateUserId(subordinateUserId);
|
if (subordinateUserId == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return userManagementRelationMapper.selectValidListBySubordinateUserId(subordinateUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得某用户当前生效的直属上级
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @return 直属上级用户,不存在则返回 null
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public AdminUserDO getDirectManager(Long userId) {
|
||||||
|
List<UserManagementRelationDO> relations = getRelationListBySubordinateUserId(userId);
|
||||||
|
if (CollUtil.isEmpty(relations)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Long managerUserId = relations.get(0).getManagerUserId();
|
||||||
|
if (managerUserId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
AdminUserDO manager = adminUserService.getUser(managerUserId);
|
||||||
|
if (!adminUserService.isUserAvailable(manager)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ spring:
|
|||||||
primary: master
|
primary: master
|
||||||
datasource:
|
datasource:
|
||||||
master:
|
master:
|
||||||
url: jdbc:mysql://192.168.1.22:13306/rdms_v3?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
url: jdbc:mysql://192.168.1.22:13306/rdms_view?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
||||||
username: root
|
username: root
|
||||||
password: njcnpqs
|
password: njcnpqs
|
||||||
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ spring:
|
|||||||
primary: master
|
primary: master
|
||||||
datasource:
|
datasource:
|
||||||
master:
|
master:
|
||||||
url: jdbc:mysql://192.168.1.22:13306/rdms_v3?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
url: jdbc:mysql://192.168.1.22:13306/rdms_view?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
||||||
username: root
|
username: root
|
||||||
password: njcnpqs
|
password: njcnpqs
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user