Compare commits
10 Commits
796c79d58f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 57061d4f03 | |||
| 5a2ca23217 | |||
| 572fc391ee | |||
| 2830b55ffb | |||
| f77007c4b7 | |||
| 5b2c6dd94c | |||
| b6c2311749 | |||
| 0a2d5ba86c | |||
| 2a5e90a4d4 | |||
| d65a7456db |
@@ -78,7 +78,8 @@
|
||||
红线:
|
||||
|
||||
- **对象内接口绝不能挂 `@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` / `ProjectExecutionAssigneeServiceImpl` 等)保持一致。
|
||||
- **第二种正规写法:属主放行 + 权限码兜底(方法体内裁决)**。任务、执行的创建 / 编辑 / 删除 / 删除预检采用"项目/执行/任务负责人身份命中直接放行,未命中回落对象域权限码"的口径,权限裁决在方法体内调 `ProjectObjectAuthorizationService`(入口见 `ProjectTaskServiceImpl.requireTaskEditDeletePermission` / `ProjectExecutionServiceImpl.requireExecutionEditDeletePermission`)。这些方法**故意不挂** `@CheckObjectPermission`——注解是前置拦截,会把"无权限码的负责人"挡死。**不要误判为漏鉴权把注解补回去**;盘点权限面时除了搜注解,还要搜 `ProjectObjectAuthorizationService` 的调用点。口径来源:`docs/superpowers/specs/2026-07-07-执行编辑删除权限口径-design.md`。
|
||||
- **同一接口不要两条通道叠加**。要么全域,要么对象域;叠加只会让对象域用户被全域那条卡死。
|
||||
- 对象内**读路径**(列表 / 详情 / 状态看板 / 聚合)同样要挂 `@CheckObjectPermission(objectType=PROJECT, permission=...PERMISSION_QUERY)`——查询同样扫库耗资源,必须按对象域鉴权。**Controller 方法层一律不挂权限注解**,新增读接口照此在 Service 层挂对象域权限;不要只在 Controller 留空,更不要误判"Controller 没注解 = 无鉴权"。
|
||||
|
||||
|
||||
79
docs/superpowers/specs/2026-07-07-执行编辑删除权限口径-design.md
Normal file
79
docs/superpowers/specs/2026-07-07-执行编辑删除权限口径-design.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# 执行编辑删除权限口径 - 设计 spec
|
||||
|
||||
- 日期:2026-07-07
|
||||
- 范围:`rdms-project` 项目对象域下执行编辑、执行删除、执行删除预检
|
||||
- 状态:已与用户确认设计,待 review 后写实现 plan
|
||||
|
||||
## 1. 背景
|
||||
|
||||
任务侧已经采用“业务身份命中直接放行,未命中再回落对象域权限码”的兼容口径。执行侧当前仍主要依赖 `@CheckObjectPermission(permission = project:execution:update/delete)`,导致执行负责人如果没有对象域权限码,可能无法编辑或删除自己负责的执行。
|
||||
|
||||
用户确认执行侧应采用类似任务侧的裁决方式:项目负责人、执行负责人可直接操作;管理员仍走对象域权限码兜底;项目创建者不作为天然放行身份。
|
||||
|
||||
## 2. 权限口径
|
||||
|
||||
执行编辑、执行删除、执行删除预检的后端最终放行条件:
|
||||
|
||||
| 操作 | 业务身份直接放行 | 权限码兜底 |
|
||||
|---|---|---|
|
||||
| 编辑执行 | 项目负责人、执行负责人 | `project:execution:update` |
|
||||
| 删除执行 | 项目负责人、执行负责人 | `project:execution:delete` |
|
||||
| 删除预检 | 项目负责人、执行负责人 | `project:execution:delete` |
|
||||
|
||||
不放行的身份:
|
||||
|
||||
- 项目创建者:只作为审计来源,不代表当前管理责任。
|
||||
- 执行协办人:参与执行,不代表可管理执行主数据。
|
||||
- 普通项目成员:除非其对象域角色拥有对应权限码。
|
||||
|
||||
这里的“管理员”不是全局超级管理员短路,而是项目对象域内拥有对应权限码的管理员型角色。当前对象域写权限不引入超管无条件绕过。
|
||||
|
||||
## 3. 后端设计
|
||||
|
||||
执行侧新增一个私有授权方法,语义与任务侧 `requireTaskEditDeletePermission` 对齐,但参与身份更少:
|
||||
|
||||
1. 加载项目与执行。
|
||||
2. 组装候选负责人:`project.managerUserId`、`execution.ownerId`。
|
||||
3. 调用 `ProjectObjectAuthorizationService.checkAnyOwnerOrProjectPermission(projectId, ownerUserIds, permission)`。
|
||||
4. 命中任一负责人直接返回;否则由 `ProjectObjectPermissionService.checkPermission` 校验对象域权限码。
|
||||
|
||||
为了让“执行负责人但无权限码”的用户能进入方法体,执行编辑、执行删除、执行删除预检方法上的 `@CheckObjectPermission(permission = ...)` 不能继续作为前置拦截;这些入口改由方法内部统一裁决。
|
||||
|
||||
保持不变:
|
||||
|
||||
- 创建执行仍走 `project:execution:create`。
|
||||
- 换执行负责人仍走项目层管理权限,不给执行负责人天然放权。
|
||||
- 执行状态动作 `complete/cancel/pause/resume` 仍保持执行负责人独占,不纳入本次编辑/删除口径。
|
||||
- 删除的业务规则不变:已完成执行禁删,非 completed 状态允许删除;删除预检仍不能被无权限用户用于探测下挂任务数量。
|
||||
|
||||
## 4. 前端联调口径
|
||||
|
||||
前端按钮展示建议:
|
||||
|
||||
- 编辑执行:`currentProject.managerUserId === currentUserId || execution.ownerId === currentUserId || hasPermission('project:execution:update')`
|
||||
- 删除执行 / 删除预检:`currentProject.managerUserId === currentUserId || execution.ownerId === currentUserId || hasPermission('project:execution:delete')`
|
||||
|
||||
后端仍是最终裁决。前端不需要识别项目创建者,也不需要把执行协办人作为执行编辑/删除按钮依据。
|
||||
|
||||
## 5. 测试与验证
|
||||
|
||||
优先补单测覆盖:
|
||||
|
||||
- 项目负责人可编辑执行,无需 `project:execution:update`。
|
||||
- 执行负责人可编辑执行,无需 `project:execution:update`。
|
||||
- 普通成员无 `project:execution:update` 时编辑被拒。
|
||||
- 拥有 `project:execution:update` 的对象域角色可编辑执行。
|
||||
- 执行负责人可删除预检和删除执行,无需 `project:execution:delete`。
|
||||
- 普通成员无 `project:execution:delete` 时删除预检和删除均被拒。
|
||||
|
||||
静态检查:
|
||||
|
||||
- 执行编辑、删除、删除预检不再由方法注解提前拦截权限码。
|
||||
- 权限裁决落在 service 层,仍属于项目对象域通道。
|
||||
- 不新增菜单、角色、权限码或数据库结构。
|
||||
|
||||
运行验证需用户同意后执行 Maven 测试命令。
|
||||
|
||||
## 6. 数据与 SQL
|
||||
|
||||
本次是纯代码权限裁决调整,不涉及表结构、字典、菜单、角色或权限码种子变更。无需演示库 SQL 补丁。
|
||||
@@ -261,6 +261,15 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode WORK_REPORT_PERIOD_DUPLICATE = new ErrorCode(1_008_010_013, "当前周期的工作报告已存在,请勿重复创建");
|
||||
ErrorCode WORK_REPORT_APPROVAL_RECORD_NOT_EXISTS = new ErrorCode(1_008_010_014, "工作报告审核记录不存在");
|
||||
ErrorCode WORK_REPORT_PROJECT_OWNER_ONLY = new ErrorCode(1_008_010_015, "仅项目负责人可创建或维护该项目的项目半月报");
|
||||
ErrorCode WORK_REPORT_SIGN_OFF_NOT_ALLOWED = new ErrorCode(1_008_010_016, "当前月报未处于待员工确认状态");
|
||||
ErrorCode WORK_REPORT_SIGN_OFF_BATCH_LOCKED = new ErrorCode(1_008_010_017, "该月份已下签,禁止重复操作");
|
||||
ErrorCode WORK_REPORT_MONTHLY_SIGN_OFF_NOT_EXISTS = new ErrorCode(1_008_010_018, "月报下签实例不存在");
|
||||
ErrorCode WORK_REPORT_MONTHLY_SIGN_OFF_CONFIRM_ONLY_REPORTER = new ErrorCode(1_008_010_019, "仅月报填报人可确认下签");
|
||||
ErrorCode WORK_REPORT_MONTHLY_SIGN_OFF_APPROVAL_RECORD_NOT_EXISTS = new ErrorCode(1_008_010_020, "月报缺少审批通过记录,无法完成下签确认");
|
||||
ErrorCode WORK_REPORT_MONTHLY_SIGN_OFF_CANDIDATE_EMPTY = new ErrorCode(1_008_010_021, "没有可下签的月报数据");
|
||||
ErrorCode SIGN_OFF_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_010_022, "当前下签状态为「{}」,不支持「{}」操作");
|
||||
ErrorCode SIGN_OFF_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_010_023, "「{}」操作必须填写原因");
|
||||
ErrorCode SIGN_OFF_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_010_024, "下签状态定义不存在或已停用");
|
||||
|
||||
// ========== 绩效管理 1_008_011_xxx ==========
|
||||
ErrorCode PERFORMANCE_TEMPLATE_NOT_EXISTS = new ErrorCode(1_008_011_001, "绩效模板不存在");
|
||||
@@ -282,4 +291,12 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PERFORMANCE_SHEET_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_011_017, "绩效表状态定义不存在或已停用");
|
||||
ErrorCode PERFORMANCE_SHEET_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_011_018, "「{}」操作必须填写原因");
|
||||
ErrorCode PERFORMANCE_EMPLOYEE_DEPT_INVALID = new ErrorCode(1_008_011_019, "被考核员工部门信息不完整");
|
||||
ErrorCode PERFORMANCE_SHEET_SIGN_OFF_NOT_ALLOWED = new ErrorCode(1_008_011_020, "当前绩效表未处于待员工确认状态");
|
||||
ErrorCode PERFORMANCE_SHEET_SIGN_OFF_BATCH_LOCKED = new ErrorCode(1_008_011_021, "该月份已下签,禁止重复操作");
|
||||
ErrorCode PERFORMANCE_SHEET_SIGN_OFF_NOT_EXISTS = new ErrorCode(1_008_011_022, "绩效下签实例不存在");
|
||||
ErrorCode PERFORMANCE_SHEET_IMPORT_ZIP_EMPTY = new ErrorCode(1_008_011_023, "导入压缩包不能为空");
|
||||
ErrorCode PERFORMANCE_SHEET_IMPORT_TARGET_EMPTY = new ErrorCode(1_008_011_024, "没有匹配到可导入的绩效表");
|
||||
ErrorCode PERFORMANCE_SHEET_SIGN_OFF_CANDIDATE_EMPTY = new ErrorCode(1_008_011_025, "没有可下签的绩效表数据");
|
||||
ErrorCode PERFORMANCE_SHEET_MONTHLY_REPORT_NOT_EXISTS = new ErrorCode(1_008_011_026, "未找到对应月报,无法同步绩效结果");
|
||||
ErrorCode PERFORMANCE_SHEET_MONTHLY_APPROVAL_RECORD_NOT_EXISTS = new ErrorCode(1_008_011_027, "未找到对应月报审批记录,无法同步绩效结果");
|
||||
}
|
||||
|
||||
@@ -36,4 +36,6 @@ public final class PerformanceConstants {
|
||||
public static final String PERMISSION_SHEET_REJECT = "project:performance-sheet:reject";
|
||||
public static final String PERMISSION_SHEET_EXPORT = "project:performance-sheet:export";
|
||||
public static final String PERMISSION_TEAM_DASHBOARD = "project:performance-sheet:team-dashboard";
|
||||
public static final String PERMISSION_SHEET_IMPORT = "project:performance-sheet:import";
|
||||
public static final String PERMISSION_SHEET_SIGN_OFF = "project:performance-sheet:sign-off";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.njcn.rdms.module.project.constant;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 绩效 Excel 单元格常量。
|
||||
*/
|
||||
public final class PerformanceExcelCellConstants {
|
||||
|
||||
private PerformanceExcelCellConstants() {
|
||||
}
|
||||
|
||||
public static final String ACTUAL_SCORE_TOTAL_CELL = "J10";
|
||||
public static final String BASE_SCORE_TOTAL_CELL = "K10";
|
||||
public static final String EXTRA_SCORE_TOTAL_CELL = "L10";
|
||||
|
||||
public static final List<String> RESULT_DESCRIPTION_CELLS = List.of("I6", "I7", "I8", "I9");
|
||||
public static final List<String> ACTUAL_SCORE_CELLS = List.of("J6", "J7", "J8", "J9", "J10");
|
||||
public static final List<String> BASE_SCORE_CELLS = List.of("K6", "K7", "K8", "K9", "K10");
|
||||
public static final List<String> EXTRA_SCORE_CELLS = List.of("L6", "L7", "L8", "L9", "L10");
|
||||
|
||||
public static final List<String> PREFILL_CELL_ADDRESSES = List.of(
|
||||
"I6", "I7", "I8", "I9",
|
||||
"J6", "J7", "J8", "J9", "J10",
|
||||
"K6", "K7", "K8", "K9", "K10",
|
||||
"L6", "L7", "L8", "L9", "L10"
|
||||
);
|
||||
|
||||
public static final List<String> MERGED_EXPORT_CELL_ADDRESSES = List.of(
|
||||
"I6", "I7", "I8", "I9",
|
||||
"J6", "J7", "J8", "J9", "J10",
|
||||
"K6", "K7", "K8", "K9", "K10",
|
||||
"L6", "L7", "L8", "L9", "L10"
|
||||
);
|
||||
}
|
||||
@@ -61,8 +61,8 @@ public final class ProjectTaskConstants {
|
||||
/**
|
||||
* 删除任务权限码。
|
||||
* 推荐挂"项目负责人"角色(参见 docs/项目/2026-05-11-执行按钮可见度对齐设计.md §6.4)。
|
||||
* 实际拦截:service 内按 task.parentTaskId 分流走 checkOwnerOrProjectPermission
|
||||
* (一级任务 → execution.ownerId 字段身份;子任务 → parentTask.ownerId 字段身份;不命中字段时回落本权限码)。
|
||||
* 实际拦截:项目负责人 / 执行负责人 / 任务负责人 / 父任务负责人直接放行;
|
||||
* 不命中业务身份时回落本权限码,作为管理员兜底。
|
||||
*/
|
||||
public static final String PERMISSION_DELETE = "project:task:delete";
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.njcn.rdms.module.project.constant;
|
||||
|
||||
/**
|
||||
* 员工下签常量。
|
||||
*/
|
||||
public final class SignOffConstants {
|
||||
|
||||
private SignOffConstants() {
|
||||
}
|
||||
|
||||
public static final String STATUS_OBJECT_TYPE = "employee_sign_off";
|
||||
|
||||
public static final String STATUS_NOT_STARTED = "not_started";
|
||||
public static final String STATUS_PENDING_EMPLOYEE_CONFIRM = "pending_employee_confirm";
|
||||
public static final String STATUS_EMPLOYEE_CONFIRMED = "employee_confirmed";
|
||||
public static final String STATUS_EMPLOYEE_REJECTED = "employee_rejected";
|
||||
|
||||
public static final String ACTION_ISSUE = "issue";
|
||||
public static final String ACTION_CONFIRM = "confirm";
|
||||
public static final String ACTION_REJECT = "reject";
|
||||
}
|
||||
@@ -31,6 +31,18 @@ public final class WorkReportConstants {
|
||||
public static final String ACTION_APPROVE = "approve";
|
||||
public static final String ACTION_REJECT = "reject";
|
||||
|
||||
/**
|
||||
* 未提交提醒附加进消息模板参数的报告类型键。
|
||||
* 仅供“未读不重发”判定使用,不参与模板正文渲染。
|
||||
*/
|
||||
public static final String PARAM_REMIND_REPORT_TYPE = "reportType";
|
||||
|
||||
/**
|
||||
* 未提交提醒附加进消息模板参数的报告标识键。
|
||||
* 统一按字符串存,避免 JSON 反序列化后的数字类型差异。
|
||||
*/
|
||||
public static final String PARAM_REMIND_REPORT_ID = "reportId";
|
||||
|
||||
public static final String PERMISSION_QUERY = "project:work-report:query";
|
||||
public static final String PERMISSION_CREATE = "project:work-report:create";
|
||||
public static final String PERMISSION_UPDATE = "project:work-report:update";
|
||||
@@ -39,4 +51,6 @@ public final class WorkReportConstants {
|
||||
public static final String PERMISSION_EXPORT = "project:work-report:export";
|
||||
public static final String PERMISSION_PROJECT_OWNER = "project:work-report:project-owner";
|
||||
public static final String PERMISSION_TEAM_DASHBOARD = "project:work-report:team-dashboard";
|
||||
public static final String PERMISSION_SIGN_OFF_QUERY = "project:work-report:sign-off-query";
|
||||
public static final String PERMISSION_SIGN_OFF_CONFIRM = "project:work-report:sign-off-confirm";
|
||||
}
|
||||
|
||||
@@ -8,9 +8,13 @@ import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceM
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetBatchDownloadReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetCreateReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetExcelUpdateReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetImportZipReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetImportZipRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetPrefillRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetResponseRecordRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetSignOffReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusDictRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusLogRespVO;
|
||||
@@ -123,6 +127,21 @@ public class PerformanceSheetController {
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/import-zip")
|
||||
@Operation(summary = "导入绩效压缩包")
|
||||
@PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_IMPORT + "')")
|
||||
public CommonResult<PerformanceSheetImportZipRespVO> importZip(@Valid PerformanceSheetImportZipReqVO reqVO)
|
||||
throws Exception {
|
||||
return success(performanceSheetService.importZip(reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/sign-off")
|
||||
@Operation(summary = "一键下签")
|
||||
@PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_SIGN_OFF + "')")
|
||||
public CommonResult<Long> signOff(@Valid @RequestBody PerformanceSheetSignOffReqVO reqVO) {
|
||||
return success(performanceSheetService.signOff(reqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/download")
|
||||
@Operation(summary = "下载单条绩效表")
|
||||
@PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_EXPORT + "')")
|
||||
@@ -162,6 +181,14 @@ public class PerformanceSheetController {
|
||||
return success(performanceSheetService.getResponseRecords(id));
|
||||
}
|
||||
|
||||
@GetMapping("/prefill")
|
||||
@Operation(summary = "获取绩效表上一份预填充数据")
|
||||
@PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_CREATE + "')")
|
||||
public CommonResult<PerformanceSheetPrefillRespVO> prefill(@RequestParam("employeeId") Long employeeId,
|
||||
@RequestParam("periodMonth") String periodMonth) {
|
||||
return success(performanceSheetService.getPrefillData(employeeId, periodMonth));
|
||||
}
|
||||
|
||||
@GetMapping("/monthly-result")
|
||||
@Operation(summary = "获取月报审批绩效结果")
|
||||
@PreAuthorize("@ss.hasPermission('" + PerformanceConstants.PERMISSION_SHEET_QUERY + "')")
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.njcn.rdms.module.project.controller.admin.performance.vo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 绩效分数单元格映射 Response VO")
|
||||
@Data
|
||||
public class PerformanceScoreCellMappingRespVO {
|
||||
@@ -15,4 +17,16 @@ public class PerformanceScoreCellMappingRespVO {
|
||||
|
||||
@Schema(description = "附加得分总计单元格", example = "L10")
|
||||
private String extraScoreTotalCell;
|
||||
|
||||
@Schema(description = "结果描述预填充单元格", example = "[\"I6\",\"I7\",\"I8\",\"I9\"]")
|
||||
private List<String> resultDescriptionCells;
|
||||
|
||||
@Schema(description = "实际得分预填充单元格", example = "[\"J6\",\"J7\",\"J8\",\"J9\",\"J10\"]")
|
||||
private List<String> actualScoreCells;
|
||||
|
||||
@Schema(description = "基础得分预填充单元格", example = "[\"K6\",\"K7\",\"K8\",\"K9\",\"K10\"]")
|
||||
private List<String> baseScoreCells;
|
||||
|
||||
@Schema(description = "附加得分预填充单元格", example = "[\"L6\",\"L7\",\"L8\",\"L9\",\"L10\"]")
|
||||
private List<String> extraScoreCells;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.performance.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 绩效压缩包导入 Request VO")
|
||||
@Data
|
||||
public class PerformanceSheetImportZipReqVO {
|
||||
|
||||
@Schema(description = "zip 文件", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "导入文件不能为空")
|
||||
private MultipartFile file;
|
||||
|
||||
@Schema(description = "绩效月份,格式 yyyy-MM", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06")
|
||||
@NotBlank(message = "绩效月份不能为空")
|
||||
private String periodMonth;
|
||||
|
||||
@Schema(description = "限定员工 ID 列表")
|
||||
private List<Long> employeeIds;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.performance.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 绩效压缩包导入结果 Response VO")
|
||||
@Data
|
||||
public class PerformanceSheetImportZipRespVO {
|
||||
|
||||
private Integer workbookCount;
|
||||
|
||||
private Integer sheetCount;
|
||||
|
||||
private Integer successCount;
|
||||
|
||||
private Integer skippedCount;
|
||||
|
||||
private Integer failedCount;
|
||||
|
||||
private List<String> messages;
|
||||
}
|
||||
@@ -38,4 +38,7 @@ public class PerformanceSheetPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "状态编码", example = "sent")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "下签状态编码", example = "pending_employee_confirm")
|
||||
private String signOffStatus;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.performance.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Schema(description = "管理后台 - 绩效表上一份预填充 Response VO")
|
||||
@Data
|
||||
public class PerformanceSheetPrefillRespVO {
|
||||
|
||||
@Schema(description = "来源绩效表 ID", example = "2042074259501088770")
|
||||
private Long sourceSheetId;
|
||||
|
||||
@Schema(description = "来源绩效月份", example = "2026-06")
|
||||
private String sourcePeriodMonth;
|
||||
|
||||
@Schema(description = "按单元格地址返回的预填充值,例如 {\"I6\":\"结果描述\",\"J10\":\"13.00\"}")
|
||||
private Map<String, String> cellValues = new LinkedHashMap<>();
|
||||
}
|
||||
@@ -25,6 +25,8 @@ public class PerformanceSheetRespVO {
|
||||
private Integer fileVersion;
|
||||
private String statusCode;
|
||||
private String statusName;
|
||||
private String signOffStatus;
|
||||
private String signOffStatusName;
|
||||
private BigDecimal actualScoreTotal;
|
||||
private BigDecimal baseScoreTotal;
|
||||
private BigDecimal extraScoreTotal;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.performance.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 绩效一键下签 Request VO")
|
||||
@Data
|
||||
public class PerformanceSheetSignOffReqVO {
|
||||
|
||||
@Schema(description = "绩效月份,格式 yyyy-MM", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06")
|
||||
@NotBlank(message = "绩效月份不能为空")
|
||||
private String periodMonth;
|
||||
|
||||
@Schema(description = "员工 ID 列表;为空则默认全部下属")
|
||||
private List<Long> employeeIds;
|
||||
|
||||
@Schema(description = "备注")
|
||||
@Size(max = 500, message = "备注长度不能超过 500 个字符")
|
||||
private String remark;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ public class MyWorkbenchController {
|
||||
}
|
||||
|
||||
@GetMapping("/worklog-week")
|
||||
@Operation(summary = "我的工时周聚合(逐日为均摊推算值;weekStart 任意日期自动归一到周一)")
|
||||
@Operation(summary = "我的工时周聚合(返回周一~周日;按周填报只均摊到工作日,周末单天工时保留在周末)")
|
||||
public CommonResult<MyWorklogWeekRespVO> getMyWorklogWeek(
|
||||
@RequestParam("weekStart")
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @NotNull(message = "weekStart 不能为空") LocalDate weekStart) {
|
||||
|
||||
@@ -15,7 +15,7 @@ public class MyWorklogWeekRespVO {
|
||||
|
||||
@Schema(description = "所选周周一", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06-08")
|
||||
private LocalDate weekStart;
|
||||
@Schema(description = "周一~周五逐日工时(固定 5 元素,均摊推算值,保留 2 位小数)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@Schema(description = "周一~周日逐日工时(固定 7 元素,按填报日期段推算,保留 2 位小数)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<BigDecimal> dailyHours;
|
||||
@Schema(description = "本周工时按归属分布(hours 降序,personal/other 排在项目后)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<DistributionItemVO> distribution;
|
||||
|
||||
@@ -5,28 +5,37 @@ import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 工作台「团队工时周聚合」Response VO")
|
||||
@Schema(description = "Admin - workbench team worklog week response")
|
||||
@Data
|
||||
public class TeamWorklogWeekRespVO {
|
||||
|
||||
@Schema(description = "所选周周一", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06-08")
|
||||
@Schema(description = "Week monday", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06-08")
|
||||
private LocalDate weekStart;
|
||||
@Schema(description = "团队成员工时列表;members[0] 恒为当前用户;该周无填报的成员 items 为空数组", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
|
||||
@Schema(description = "Team members", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<MemberVO> members;
|
||||
|
||||
@Schema(description = "单个成员的周工时")
|
||||
@Schema(description = "Single member week worklog")
|
||||
@Data
|
||||
public static class MemberVO {
|
||||
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
|
||||
|
||||
@Schema(description = "User id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long userId;
|
||||
@Schema(description = "用户昵称", example = "张三")
|
||||
|
||||
@Schema(description = "User nickname", example = "zhangsan")
|
||||
private String userNickname;
|
||||
@Schema(description = "本周工时按归属分布", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
|
||||
@Schema(description = "Weekly worklog distribution", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<MyWorklogWeekRespVO.DistributionItemVO> items;
|
||||
|
||||
@Schema(description = "Approved overtime hours in selected week",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED, example = "9.00")
|
||||
private BigDecimal overtimeHours;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ public class ProjectTaskController {
|
||||
}
|
||||
|
||||
@DeleteMapping("/{taskId}")
|
||||
@Operation(summary = "删除任务(已完成态禁止,三重确认 + 上级 owner 硬卡)")
|
||||
@Operation(summary = "删除任务(已完成态禁止,三重确认 + 多身份授权)")
|
||||
public CommonResult<Boolean> deleteTask(@PathVariable("projectId") Long projectId,
|
||||
@PathVariable("executionId") Long executionId,
|
||||
@PathVariable("taskId") Long taskId,
|
||||
|
||||
@@ -17,6 +17,8 @@ import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.Month
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRefreshDraftReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSaveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSignOffConfirmReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSignOffDetailRespVO;
|
||||
import com.njcn.rdms.module.project.service.workreport.export.WorkReportContentExportService;
|
||||
import com.njcn.rdms.module.project.service.workreport.monthly.MonthlyReportService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -111,6 +113,20 @@ public class MonthlyReportController {
|
||||
return success(monthlyReportService.getMonthlyApprovalPage(reqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/sign-off-page")
|
||||
@Operation(summary = "获取待我确认的月报分页")
|
||||
@PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_SIGN_OFF_QUERY + "')")
|
||||
public CommonResult<PageResult<MonthlyReportRespVO>> getMonthlySignOffPage(@Valid MonthlyReportPageReqVO reqVO) {
|
||||
return success(monthlyReportService.getMonthlySignOffPage(reqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/sign-off-detail")
|
||||
@Operation(summary = "获取月报下签详情")
|
||||
@PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_SIGN_OFF_QUERY + "')")
|
||||
public CommonResult<MonthlyReportSignOffDetailRespVO> getMonthlySignOffDetail(@PathVariable("id") Long id) {
|
||||
return success(monthlyReportService.getMonthlySignOffDetail(id));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/submit")
|
||||
@Operation(summary = "提交月报")
|
||||
@PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_UPDATE + "')")
|
||||
@@ -137,6 +153,15 @@ public class MonthlyReportController {
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/confirm-sign-off")
|
||||
@Operation(summary = "确认月报下签")
|
||||
@PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_SIGN_OFF_CONFIRM + "')")
|
||||
public CommonResult<Boolean> confirmMonthlySignOff(@PathVariable("id") Long id,
|
||||
@Valid @RequestBody MonthlyReportSignOffConfirmReqVO reqVO) {
|
||||
monthlyReportService.confirmMonthlySignOff(id, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "删除月报")
|
||||
@PreAuthorize("@ss.hasPermission('" + WorkReportConstants.PERMISSION_DELETE + "')")
|
||||
|
||||
@@ -36,10 +36,14 @@ public class MonthlyReportApprovalRecordRespVO {
|
||||
|
||||
private String employeeSignName;
|
||||
|
||||
private Long employeeSignatureFileId;
|
||||
|
||||
private LocalDate employeeSignedDate;
|
||||
|
||||
private String supervisorSignName;
|
||||
|
||||
private Long supervisorSignatureFileId;
|
||||
|
||||
private LocalDate supervisorSignedDate;
|
||||
|
||||
private Long auditorUserId;
|
||||
|
||||
@@ -14,4 +14,7 @@ public class MonthlyReportPageReqVO extends WorkReportBasePageReqVO {
|
||||
|
||||
@Schema(description = "团队视角下的填报人用户编号列表")
|
||||
private List<Long> reporterIds;
|
||||
|
||||
@Schema(description = "下签状态编码", example = "pending_employee_confirm")
|
||||
private String signOffStatus;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ public class MonthlyReportRespVO {
|
||||
private LocalDate periodEndDate;
|
||||
private String statusCode;
|
||||
private String statusName;
|
||||
private String signOffStatus;
|
||||
private String signOffStatusName;
|
||||
private Boolean allowEdit;
|
||||
private Boolean terminal;
|
||||
private BigDecimal totalWorkHours;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
|
||||
|
||||
@Schema(description = "管理后台 - 月报下签确认 Request VO")
|
||||
@Data
|
||||
public class MonthlyReportSignOffConfirmReqVO {
|
||||
|
||||
@Schema(description = "面谈日期", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "面谈日期不能为空")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
|
||||
private LocalDate meetingDate;
|
||||
|
||||
@Schema(description = "员工签字日期")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
|
||||
private LocalDate employeeSignedDate;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 月报下签详情 Response VO")
|
||||
@Data
|
||||
public class MonthlyReportSignOffDetailRespVO {
|
||||
|
||||
private MonthlyReportRespVO report;
|
||||
|
||||
private MonthlyReportApprovalRecordRespVO latestApprovalRecord;
|
||||
}
|
||||
@@ -46,6 +46,8 @@ public class PerformanceSheetDO extends BaseDO {
|
||||
|
||||
private String statusCode;
|
||||
|
||||
private String signOffStatus;
|
||||
|
||||
private BigDecimal actualScoreTotal;
|
||||
|
||||
private BigDecimal baseScoreTotal;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.performance;
|
||||
|
||||
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.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 绩效下签当前实例。
|
||||
*/
|
||||
@TableName("rdms_performance_sheet_sign_off")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PerformanceSheetSignOffDO extends BaseDO {
|
||||
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
private Long performanceSheetId;
|
||||
|
||||
private Long batchNo;
|
||||
|
||||
private String statusCode;
|
||||
|
||||
private Long issueUserId;
|
||||
|
||||
private String issueUserName;
|
||||
|
||||
private LocalDateTime issueTime;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private Long confirmUserId;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String confirmUserName;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private LocalDateTime confirmTime;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String lastRejectReason;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.performance;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 绩效下签操作记录。
|
||||
*/
|
||||
@TableName("rdms_performance_sheet_sign_off_record")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PerformanceSheetSignOffRecordDO extends BaseDO {
|
||||
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
private Long signOffId;
|
||||
|
||||
private Long performanceSheetId;
|
||||
|
||||
private Long batchNo;
|
||||
|
||||
private String actionType;
|
||||
|
||||
private String fromSignOffStatus;
|
||||
|
||||
private String toSignOffStatus;
|
||||
|
||||
private String reason;
|
||||
|
||||
private Long operatorUserId;
|
||||
|
||||
private String operatorName;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.signoff;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 部门月份下签锁定批次。
|
||||
*/
|
||||
@TableName("rdms_employee_sign_off_batch")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class EmployeeSignOffBatchDO extends BaseDO {
|
||||
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
private String periodMonth;
|
||||
|
||||
private Long deptId;
|
||||
|
||||
private String deptName;
|
||||
|
||||
private Long operatorUserId;
|
||||
|
||||
private String operatorName;
|
||||
|
||||
private Integer lockedFlag;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
@@ -56,12 +56,18 @@ public class MonthlyReportApprovalRecordDO extends BaseDO {
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String employeeSignName;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private Long employeeSignatureFileId;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private LocalDate employeeSignedDate;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String supervisorSignName;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private Long supervisorSignatureFileId;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private LocalDate supervisorSignedDate;
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ public class MonthlyReportDO extends BaseDO {
|
||||
|
||||
private String statusCode;
|
||||
|
||||
private String signOffStatus;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private BigDecimal totalWorkHours;
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.workreport.monthly;
|
||||
|
||||
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.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 月报下签当前实例。
|
||||
*/
|
||||
@TableName("rdms_work_report_monthly_sign_off")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class MonthlyReportSignOffDO extends BaseDO {
|
||||
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
private Long monthlyReportId;
|
||||
|
||||
private Long batchNo;
|
||||
|
||||
private String statusCode;
|
||||
|
||||
private Long issueUserId;
|
||||
|
||||
private String issueUserName;
|
||||
|
||||
private LocalDateTime issueTime;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private Long confirmUserId;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String confirmUserName;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private LocalDateTime confirmTime;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.workreport.monthly;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 月报下签操作记录。
|
||||
*/
|
||||
@TableName("rdms_work_report_monthly_sign_off_record")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class MonthlyReportSignOffRecordDO extends BaseDO {
|
||||
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
private Long signOffId;
|
||||
|
||||
private Long monthlyReportId;
|
||||
|
||||
private Long approvalRecordId;
|
||||
|
||||
private Long batchNo;
|
||||
|
||||
private String actionType;
|
||||
|
||||
private String fromSignOffStatus;
|
||||
|
||||
private String toSignOffStatus;
|
||||
|
||||
private Long operatorUserId;
|
||||
|
||||
private String operatorName;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Mapper
|
||||
public interface OvertimeApplicationMapper extends BaseMapperX<OvertimeApplicationDO> {
|
||||
@@ -53,6 +54,20 @@ public interface OvertimeApplicationMapper extends BaseMapperX<OvertimeApplicati
|
||||
.eq(OvertimeApplicationDO::getApplicantId, applicantId));
|
||||
}
|
||||
|
||||
default List<OvertimeApplicationDO> selectApprovedListByApplicantIdsAndDateRange(Collection<Long> applicantIds,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate,
|
||||
String approvedStatusCode) {
|
||||
if (applicantIds == null || applicantIds.isEmpty() || startDate == null || endDate == null
|
||||
|| !StringUtils.hasText(approvedStatusCode)) {
|
||||
return List.of();
|
||||
}
|
||||
return selectList(new LambdaQueryWrapperX<OvertimeApplicationDO>()
|
||||
.in(OvertimeApplicationDO::getApplicantId, applicantIds)
|
||||
.eq(OvertimeApplicationDO::getStatusCode, approvedStatusCode)
|
||||
.between(OvertimeApplicationDO::getOvertimeDate, startDate, endDate));
|
||||
}
|
||||
|
||||
default int updateByIdAndStatus(OvertimeApplicationDO update, Long id, String fromStatus) {
|
||||
return update(update, new LambdaQueryWrapperX<OvertimeApplicationDO>()
|
||||
.eq(OvertimeApplicationDO::getId, id)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.performance;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
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.mapper.BaseMapperX;
|
||||
import com.njcn.rdms.module.project.constant.PerformanceConstants;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetPageReqVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceSheetDO;
|
||||
@@ -20,6 +20,16 @@ public interface PerformanceSheetMapper extends BaseMapperX<PerformanceSheetDO>
|
||||
.eq(PerformanceSheetDO::getPeriodMonth, periodMonth));
|
||||
}
|
||||
|
||||
default PerformanceSheetDO selectLatestHistoryWithFile(Long employeeId, String beforePeriodMonth) {
|
||||
return selectOne(new LambdaQueryWrapperX<PerformanceSheetDO>()
|
||||
.eq(PerformanceSheetDO::getEmployeeId, employeeId)
|
||||
.lt(PerformanceSheetDO::getPeriodMonth, beforePeriodMonth)
|
||||
.isNotNull(PerformanceSheetDO::getFileId)
|
||||
.orderByDesc(PerformanceSheetDO::getPeriodMonth)
|
||||
.orderByDesc(PerformanceSheetDO::getId)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
default PageResult<PerformanceSheetDO> selectEmployeePage(Long employeeId, PerformanceSheetPageReqVO reqVO) {
|
||||
LambdaQueryWrapperX<PerformanceSheetDO> wrapper = buildPageQuery(reqVO)
|
||||
.eq(PerformanceSheetDO::getEmployeeId, employeeId)
|
||||
@@ -92,6 +102,14 @@ public interface PerformanceSheetMapper extends BaseMapperX<PerformanceSheetDO>
|
||||
.eq(PerformanceSheetDO::getStatusCode, fromStatus));
|
||||
}
|
||||
|
||||
default int updateByIdAndStatusAndSignOffStatus(PerformanceSheetDO update, Long id, String fromStatus,
|
||||
String fromSignOffStatus) {
|
||||
return update(update, new LambdaQueryWrapperX<PerformanceSheetDO>()
|
||||
.eq(PerformanceSheetDO::getId, id)
|
||||
.eq(PerformanceSheetDO::getStatusCode, fromStatus)
|
||||
.eq(PerformanceSheetDO::getSignOffStatus, fromSignOffStatus));
|
||||
}
|
||||
|
||||
private LambdaQueryWrapperX<PerformanceSheetDO> buildPageQuery(PerformanceSheetPageReqVO reqVO) {
|
||||
return new LambdaQueryWrapperX<PerformanceSheetDO>()
|
||||
.geIfPresent(PerformanceSheetDO::getPeriodMonth, reqVO.getPeriodMonthStart())
|
||||
@@ -101,6 +119,7 @@ public interface PerformanceSheetMapper extends BaseMapperX<PerformanceSheetDO>
|
||||
.likeIfPresent(PerformanceSheetDO::getEmployeeDeptName, reqVO.getEmployeeDeptName())
|
||||
.eqIfPresent(PerformanceSheetDO::getManagerId, reqVO.getManagerId())
|
||||
.likeIfPresent(PerformanceSheetDO::getManagerName, reqVO.getManagerName())
|
||||
.eqIfPresent(PerformanceSheetDO::getStatusCode, reqVO.getStatusCode());
|
||||
.eqIfPresent(PerformanceSheetDO::getStatusCode, reqVO.getStatusCode())
|
||||
.eqIfPresent(PerformanceSheetDO::getSignOffStatus, reqVO.getSignOffStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.performance;
|
||||
|
||||
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.performance.PerformanceSheetSignOffDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PerformanceSheetSignOffMapper extends BaseMapperX<PerformanceSheetSignOffDO> {
|
||||
|
||||
default PerformanceSheetSignOffDO selectByPerformanceSheetId(Long performanceSheetId) {
|
||||
return selectOne(new LambdaQueryWrapperX<PerformanceSheetSignOffDO>()
|
||||
.eq(PerformanceSheetSignOffDO::getPerformanceSheetId, performanceSheetId));
|
||||
}
|
||||
|
||||
default int updateByPerformanceSheetIdAndStatus(PerformanceSheetSignOffDO update, Long performanceSheetId,
|
||||
String fromStatus) {
|
||||
return update(update, new LambdaQueryWrapperX<PerformanceSheetSignOffDO>()
|
||||
.eq(PerformanceSheetSignOffDO::getPerformanceSheetId, performanceSheetId)
|
||||
.eq(PerformanceSheetSignOffDO::getStatusCode, fromStatus));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.performance;
|
||||
|
||||
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceSheetSignOffRecordDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PerformanceSheetSignOffRecordMapper extends BaseMapperX<PerformanceSheetSignOffRecordDO> {
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.signoff;
|
||||
|
||||
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.signoff.EmployeeSignOffBatchDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface EmployeeSignOffBatchMapper extends BaseMapperX<EmployeeSignOffBatchDO> {
|
||||
|
||||
default EmployeeSignOffBatchDO selectByPeriodMonthAndDeptId(String periodMonth, Long deptId) {
|
||||
return selectOne(new LambdaQueryWrapperX<EmployeeSignOffBatchDO>()
|
||||
.eq(EmployeeSignOffBatchDO::getPeriodMonth, periodMonth)
|
||||
.eq(EmployeeSignOffBatchDO::getDeptId, deptId));
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,13 @@ public interface MonthlyReportApprovalRecordMapper extends BaseMapperX<MonthlyRe
|
||||
return delete(new LambdaQueryWrapperX<MonthlyReportApprovalRecordDO>()
|
||||
.eq(MonthlyReportApprovalRecordDO::getMonthlyReportId, monthlyReportId));
|
||||
}
|
||||
|
||||
default MonthlyReportApprovalRecordDO selectLatestApprovedByMonthlyReportId(Long monthlyReportId) {
|
||||
return selectOne(new LambdaQueryWrapperX<MonthlyReportApprovalRecordDO>()
|
||||
.eq(MonthlyReportApprovalRecordDO::getMonthlyReportId, monthlyReportId)
|
||||
.eq(MonthlyReportApprovalRecordDO::getConclusion, "approved")
|
||||
.orderByDesc(MonthlyReportApprovalRecordDO::getApprovalRound)
|
||||
.orderByDesc(MonthlyReportApprovalRecordDO::getId)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +147,14 @@ public interface MonthlyReportMapper extends BaseMapperX<MonthlyReportDO> {
|
||||
.eq(MonthlyReportDO::getStatusCode, fromStatus));
|
||||
}
|
||||
|
||||
default int updateByIdAndStatusAndSignOffStatus(MonthlyReportDO update, Long id, String fromStatus,
|
||||
String fromSignOffStatus) {
|
||||
return update(update, new LambdaQueryWrapperX<MonthlyReportDO>()
|
||||
.eq(MonthlyReportDO::getId, id)
|
||||
.eq(MonthlyReportDO::getStatusCode, fromStatus)
|
||||
.eq(MonthlyReportDO::getSignOffStatus, fromSignOffStatus));
|
||||
}
|
||||
|
||||
default int updateByIdAndStatusesAndReporterId(MonthlyReportDO update, Long id, Collection<String> statuses,
|
||||
Long reporterId) {
|
||||
return update(update, new LambdaQueryWrapperX<MonthlyReportDO>()
|
||||
@@ -177,6 +185,7 @@ public interface MonthlyReportMapper extends BaseMapperX<MonthlyReportDO> {
|
||||
private LambdaQueryWrapperX<MonthlyReportDO> buildPageQuery(MonthlyReportPageReqVO reqVO) {
|
||||
LambdaQueryWrapperX<MonthlyReportDO> wrapper = new LambdaQueryWrapperX<MonthlyReportDO>()
|
||||
.eqIfPresent(MonthlyReportDO::getStatusCode, reqVO.getStatusCode())
|
||||
.eqIfPresent(MonthlyReportDO::getSignOffStatus, reqVO.getSignOffStatus())
|
||||
.betweenIfPresent(MonthlyReportDO::getPeriodStartDate, reqVO.getPeriodStartDate())
|
||||
.betweenIfPresent(MonthlyReportDO::getSubmitTime, reqVO.getSubmitTime());
|
||||
if (StringUtils.hasText(reqVO.getKeyword())) {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.workreport.monthly;
|
||||
|
||||
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.workreport.monthly.MonthlyReportSignOffDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface MonthlyReportSignOffMapper extends BaseMapperX<MonthlyReportSignOffDO> {
|
||||
|
||||
default MonthlyReportSignOffDO selectByMonthlyReportId(Long monthlyReportId) {
|
||||
return selectOne(new LambdaQueryWrapperX<MonthlyReportSignOffDO>()
|
||||
.eq(MonthlyReportSignOffDO::getMonthlyReportId, monthlyReportId));
|
||||
}
|
||||
|
||||
default int updateByMonthlyReportIdAndStatus(MonthlyReportSignOffDO update, Long monthlyReportId,
|
||||
String fromStatus) {
|
||||
return update(update, new LambdaQueryWrapperX<MonthlyReportSignOffDO>()
|
||||
.eq(MonthlyReportSignOffDO::getMonthlyReportId, monthlyReportId)
|
||||
.eq(MonthlyReportSignOffDO::getStatusCode, fromStatus));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.workreport.monthly;
|
||||
|
||||
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportSignOffRecordDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface MonthlyReportSignOffRecordMapper extends BaseMapperX<MonthlyReportSignOffRecordDO> {
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import com.njcn.rdms.module.system.api.permission.PermissionApi;
|
||||
import com.njcn.rdms.module.system.api.permission.UserVisibilityConfigApi;
|
||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||
import com.njcn.rdms.module.system.api.user.UserManagementRelationApi;
|
||||
import com.njcn.rdms.module.system.api.user.UserSignatureApi;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@@ -22,6 +23,6 @@ import org.springframework.context.annotation.Configuration;
|
||||
{AdminUserApi.class, ObjectPermissionApi.class, DictDataApi.class, FileApi.class,
|
||||
PermissionApi.class, OrgLeaderApi.class, UserVisibilityConfigApi.class,
|
||||
DeptApi.class, PostApi.class, UserManagementRelationApi.class,
|
||||
NotifyMessageSendApi.class})
|
||||
NotifyMessageSendApi.class, UserSignatureApi.class})
|
||||
public class RpcConfiguration {
|
||||
}
|
||||
|
||||
@@ -4,10 +4,17 @@ import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 项目对象业务授权服务。
|
||||
* 项目对象业务授权服务:属主(业务负责人)身份命中直接放行,未命中回落对象域权限码兜底。
|
||||
*
|
||||
* <p>供任务 / 执行的创建、编辑、删除、删除预检等入口在<b>方法体内</b>调用,替代方法上的
|
||||
* {@code @CheckObjectPermission} 前置拦截——注解拦截发生在进入方法体之前,无法先认负责人身份。
|
||||
*
|
||||
* <p>红线:调用本服务做裁决的 service 方法<b>不要再挂 {@code @CheckObjectPermission}</b>,
|
||||
* 叠加会把"无权限码的负责人"在注解层直接挡死。口径登记见 CLAUDE.md §鉴权。
|
||||
*/
|
||||
@Service
|
||||
public class ProjectObjectAuthorizationService {
|
||||
@@ -26,4 +33,15 @@ public class ProjectObjectAuthorizationService {
|
||||
projectObjectPermissionService.checkPermission(projectId, permission, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前登录人命中任一业务负责人时直接放行,否则回落到项目对象角色权限校验。
|
||||
*/
|
||||
public void checkAnyOwnerOrProjectPermission(Long projectId, Collection<Long> ownerUserIds, String permission) {
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
if (ownerUserIds != null && ownerUserIds.stream().anyMatch(ownerUserId -> Objects.equals(loginUserId, ownerUserId))) {
|
||||
return;
|
||||
}
|
||||
projectObjectPermissionService.checkPermission(projectId, permission, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.njcn.rdms.module.project.service.performance;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 绩效配置。
|
||||
*/
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "rdms.performance")
|
||||
@Data
|
||||
public class PerformanceProperties {
|
||||
|
||||
private ScoreCellMapping scoreCellMapping = new ScoreCellMapping();
|
||||
|
||||
@Data
|
||||
public static class ScoreCellMapping {
|
||||
|
||||
private String actualScoreTotal = "J10";
|
||||
|
||||
private String baseScoreTotal = "K10";
|
||||
|
||||
private String extraScoreTotal = "L10";
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,13 @@ import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceMonthlyResultRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetCreateReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetExcelUpdateReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetImportZipReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetImportZipRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetPrefillRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetResponseRecordRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetSignOffReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusDictRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusLogRespVO;
|
||||
@@ -35,6 +39,10 @@ public interface PerformanceSheetService {
|
||||
|
||||
void reject(Long id, PerformanceSheetStatusActionReqVO reqVO);
|
||||
|
||||
PerformanceSheetImportZipRespVO importZip(PerformanceSheetImportZipReqVO reqVO) throws Exception;
|
||||
|
||||
Long signOff(PerformanceSheetSignOffReqVO reqVO);
|
||||
|
||||
PerformanceDownloadFile download(Long id);
|
||||
|
||||
PerformanceDownloadFile batchDownload(Collection<Long> ids);
|
||||
@@ -45,6 +53,8 @@ public interface PerformanceSheetService {
|
||||
|
||||
List<PerformanceSheetResponseRecordRespVO> getResponseRecords(Long id);
|
||||
|
||||
PerformanceSheetPrefillRespVO getPrefillData(Long employeeId, String periodMonth);
|
||||
|
||||
PerformanceMonthlyResultRespVO getMonthlyResult(Long employeeId, String periodMonth);
|
||||
|
||||
List<PerformanceSheetStatusDictRespVO> getStatusDict();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ package com.njcn.rdms.module.project.service.performance;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||
import com.njcn.rdms.module.project.constant.PerformanceExcelCellConstants;
|
||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceScoreCellMappingRespVO;
|
||||
@@ -25,8 +26,6 @@ public class PerformanceTemplateServiceImpl implements PerformanceTemplateServic
|
||||
|
||||
@Resource
|
||||
private PerformanceTemplateMapper performanceTemplateMapper;
|
||||
@Resource
|
||||
private PerformanceProperties performanceProperties;
|
||||
|
||||
@Override
|
||||
public PerformanceTemplateRespVO getCurrentTemplate() {
|
||||
@@ -98,11 +97,14 @@ public class PerformanceTemplateServiceImpl implements PerformanceTemplateServic
|
||||
}
|
||||
|
||||
private PerformanceScoreCellMappingRespVO scoreCellMapping() {
|
||||
PerformanceProperties.ScoreCellMapping mapping = performanceProperties.getScoreCellMapping();
|
||||
PerformanceScoreCellMappingRespVO respVO = new PerformanceScoreCellMappingRespVO();
|
||||
respVO.setActualScoreTotalCell(mapping.getActualScoreTotal());
|
||||
respVO.setBaseScoreTotalCell(mapping.getBaseScoreTotal());
|
||||
respVO.setExtraScoreTotalCell(mapping.getExtraScoreTotal());
|
||||
respVO.setActualScoreTotalCell(PerformanceExcelCellConstants.ACTUAL_SCORE_TOTAL_CELL);
|
||||
respVO.setBaseScoreTotalCell(PerformanceExcelCellConstants.BASE_SCORE_TOTAL_CELL);
|
||||
respVO.setExtraScoreTotalCell(PerformanceExcelCellConstants.EXTRA_SCORE_TOTAL_CELL);
|
||||
respVO.setResultDescriptionCells(PerformanceExcelCellConstants.RESULT_DESCRIPTION_CELLS);
|
||||
respVO.setActualScoreCells(PerformanceExcelCellConstants.ACTUAL_SCORE_CELLS);
|
||||
respVO.setBaseScoreCells(PerformanceExcelCellConstants.BASE_SCORE_CELLS);
|
||||
respVO.setExtraScoreCells(PerformanceExcelCellConstants.EXTRA_SCORE_CELLS);
|
||||
return respVO;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
package com.njcn.rdms.module.project.service.project;
|
||||
|
||||
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.project.project.vo.workbench.MyWorklogWeekRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.project.vo.workbench.TeamWorklogWeekRespVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.personal.PersonalItemDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskWorklogDO;
|
||||
import com.njcn.rdms.module.project.dal.mysql.overtime.OvertimeApplicationMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.personal.PersonalItemMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.project.task.TaskWorklogMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
@@ -31,6 +35,8 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class MyWorklogServiceImpl implements MyWorklogService {
|
||||
|
||||
private static final BigDecimal OVERTIME_DAY_HOURS = new BigDecimal("6");
|
||||
|
||||
@Resource
|
||||
private TaskWorklogMapper taskWorklogMapper;
|
||||
@Resource
|
||||
@@ -41,29 +47,29 @@ public class MyWorklogServiceImpl implements MyWorklogService {
|
||||
private ProjectMapper projectMapper;
|
||||
@Resource
|
||||
private MyTeamService myTeamService;
|
||||
@Resource
|
||||
private OvertimeApplicationMapper overtimeApplicationMapper;
|
||||
|
||||
@Override
|
||||
public MyWorklogWeekRespVO getMyWorklogWeek(LocalDate weekStart) {
|
||||
Long me = SecurityFrameworkUtils.getLoginUserId();
|
||||
LocalDate monday = MyWorklogWeekSupport.normalizeToMonday(weekStart);
|
||||
// 查询区间含周末:周六/周日的段要兜底归周五,必须查回来
|
||||
List<TaskWorklogDO> worklogs = taskWorklogMapper
|
||||
.selectListByUserIdAndPeriod(me, monday, monday.plusDays(6));
|
||||
// 逐日累计(scale=4)与每任务份额累计
|
||||
Map<LocalDate, BigDecimal> daily = new LinkedHashMap<>();
|
||||
Map<Long, BigDecimal> hoursByTask = new HashMap<>();
|
||||
for (TaskWorklogDO w : worklogs) {
|
||||
for (TaskWorklogDO worklog : worklogs) {
|
||||
Map<LocalDate, BigDecimal> shares = MyWorklogWeekSupport
|
||||
.apportionToWeek(w.getStartDate(), w.getEndDate(), w.getDurationHours(), monday);
|
||||
for (Map.Entry<LocalDate, BigDecimal> e : shares.entrySet()) {
|
||||
daily.merge(e.getKey(), e.getValue(), BigDecimal::add);
|
||||
hoursByTask.merge(w.getTaskId(), e.getValue(), BigDecimal::add);
|
||||
.apportionToWeek(worklog.getStartDate(), worklog.getEndDate(), worklog.getDurationHours(), monday);
|
||||
for (Map.Entry<LocalDate, BigDecimal> entry : shares.entrySet()) {
|
||||
daily.merge(entry.getKey(), entry.getValue(), BigDecimal::add);
|
||||
hoursByTask.merge(worklog.getTaskId(), entry.getValue(), BigDecimal::add);
|
||||
}
|
||||
}
|
||||
MyWorklogWeekRespVO resp = new MyWorklogWeekRespVO();
|
||||
resp.setWeekStart(monday);
|
||||
List<BigDecimal> dailyHours = new ArrayList<>(5);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
List<BigDecimal> dailyHours = new ArrayList<>(7);
|
||||
for (int i = 0; i < 7; i++) {
|
||||
dailyHours.add(daily.getOrDefault(monday.plusDays(i), BigDecimal.ZERO)
|
||||
.setScale(2, RoundingMode.HALF_UP));
|
||||
}
|
||||
@@ -75,40 +81,82 @@ public class MyWorklogServiceImpl implements MyWorklogService {
|
||||
@Override
|
||||
public TeamWorklogWeekRespVO getTeamWorklogWeek(LocalDate weekStart) {
|
||||
LocalDate monday = MyWorklogWeekSupport.normalizeToMonday(weekStart);
|
||||
LocalDate sunday = monday.plusDays(6);
|
||||
List<MyTeamService.TeamMember> members = myTeamService.resolveTeamMembers();
|
||||
List<Long> userIds = members.stream().map(MyTeamService.TeamMember::userId).collect(Collectors.toList());
|
||||
List<TaskWorklogDO> worklogs = taskWorklogMapper
|
||||
.selectListByUserIdsAndPeriod(userIds, monday, monday.plusDays(6));
|
||||
// 每成员每任务的本周份额合计
|
||||
List<TaskWorklogDO> worklogs = taskWorklogMapper.selectListByUserIdsAndPeriod(userIds, monday, sunday);
|
||||
|
||||
Map<Long, Map<Long, BigDecimal>> hoursByUserAndTask = new LinkedHashMap<>();
|
||||
for (TaskWorklogDO w : worklogs) {
|
||||
for (TaskWorklogDO worklog : worklogs) {
|
||||
BigDecimal weekTotal = MyWorklogWeekSupport
|
||||
.apportionToWeek(w.getStartDate(), w.getEndDate(), w.getDurationHours(), monday)
|
||||
.apportionToWeek(worklog.getStartDate(), worklog.getEndDate(), worklog.getDurationHours(), monday)
|
||||
.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
if (weekTotal.signum() > 0) {
|
||||
hoursByUserAndTask.computeIfAbsent(w.getUserId(), k -> new HashMap<>())
|
||||
.merge(w.getTaskId(), weekTotal, BigDecimal::add);
|
||||
hoursByUserAndTask.computeIfAbsent(worklog.getUserId(), key -> new HashMap<>())
|
||||
.merge(worklog.getTaskId(), weekTotal, BigDecimal::add);
|
||||
}
|
||||
}
|
||||
|
||||
Map<Long, BigDecimal> overtimeHoursByUser = buildApprovedOvertimeHoursByUser(userIds, monday, sunday);
|
||||
TeamWorklogWeekRespVO resp = new TeamWorklogWeekRespVO();
|
||||
resp.setWeekStart(monday);
|
||||
resp.setMembers(members.stream().map(m -> {
|
||||
resp.setMembers(members.stream().map(member -> {
|
||||
TeamWorklogWeekRespVO.MemberVO vo = new TeamWorklogWeekRespVO.MemberVO();
|
||||
vo.setUserId(m.userId());
|
||||
vo.setUserNickname(m.nickname());
|
||||
vo.setUserId(member.userId());
|
||||
vo.setUserNickname(member.nickname());
|
||||
vo.setItems(buildDistribution(
|
||||
hoursByUserAndTask.getOrDefault(m.userId(), Collections.emptyMap())));
|
||||
hoursByUserAndTask.getOrDefault(member.userId(), Collections.emptyMap())));
|
||||
vo.setOvertimeHours(overtimeHoursByUser.getOrDefault(member.userId(), BigDecimal.ZERO)
|
||||
.setScale(2, RoundingMode.HALF_UP));
|
||||
return vo;
|
||||
}).collect(Collectors.toList()));
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 taskId→本周工时 聚成归属分布:工时表的 task_id 既可能指向任务也可能指向个人事项
|
||||
* (个人事项工时复用 rdms_task_worklog),先按任务表归 project,再按个人事项表归 personal,
|
||||
* 都对不上归 other。排序:project 行按 hours 降序,personal/other 殿后。
|
||||
* 被软删的任务/事项的残留工时归 other(两表主键同为雪花 id 全局不撞,不存在误归类)。
|
||||
*/
|
||||
private Map<Long, BigDecimal> buildApprovedOvertimeHoursByUser(List<Long> userIds,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate) {
|
||||
if (userIds == null || userIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<OvertimeApplicationDO> applications = overtimeApplicationMapper.selectApprovedListByApplicantIdsAndDateRange(
|
||||
userIds, startDate, endDate, OvertimeApplicationConstants.STATUS_APPROVED);
|
||||
if (applications.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<Long, BigDecimal> overtimeHoursByUser = new HashMap<>();
|
||||
for (OvertimeApplicationDO application : applications) {
|
||||
if (application == null || application.getApplicantId() == null) {
|
||||
continue;
|
||||
}
|
||||
BigDecimal overtimeHours = parseOvertimeHours(application.getOvertimeDuration());
|
||||
if (overtimeHours.signum() <= 0) {
|
||||
continue;
|
||||
}
|
||||
overtimeHoursByUser.merge(application.getApplicantId(), overtimeHours, BigDecimal::add);
|
||||
}
|
||||
return overtimeHoursByUser;
|
||||
}
|
||||
|
||||
private BigDecimal parseOvertimeHours(String overtimeDuration) {
|
||||
if (!StringUtils.hasText(overtimeDuration)) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
String normalized = overtimeDuration.trim()
|
||||
.replace("\u5929", "")
|
||||
.replace("\u5c0f\u65f6", "")
|
||||
.replace("h", "")
|
||||
.trim();
|
||||
if (!StringUtils.hasText(normalized)) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(normalized).multiply(OVERTIME_DAY_HOURS);
|
||||
} catch (NumberFormatException ex) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
private List<MyWorklogWeekRespVO.DistributionItemVO> buildDistribution(Map<Long, BigDecimal> hoursByTask) {
|
||||
if (hoursByTask.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
@@ -117,37 +165,41 @@ public class MyWorklogServiceImpl implements MyWorklogService {
|
||||
Map<Long, ProjectTaskDO> taskMap = projectTaskMapper.selectBatchIds(taskIds).stream()
|
||||
.collect(Collectors.toMap(ProjectTaskDO::getId, Function.identity(), (a, b) -> a));
|
||||
Set<Long> personalIds = taskIds.stream()
|
||||
.filter(id -> !taskMap.containsKey(id)).collect(Collectors.toSet());
|
||||
.filter(id -> !taskMap.containsKey(id))
|
||||
.collect(Collectors.toSet());
|
||||
Set<Long> personalHit = personalIds.isEmpty() ? Collections.emptySet()
|
||||
: personalItemMapper.selectBatchIds(personalIds).stream()
|
||||
.map(PersonalItemDO::getId).collect(Collectors.toSet());
|
||||
// 聚合:projectId→hours / personal / other
|
||||
.map(PersonalItemDO::getId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Map<Long, BigDecimal> hoursByProject = new LinkedHashMap<>();
|
||||
BigDecimal personalHours = BigDecimal.ZERO;
|
||||
BigDecimal otherHours = BigDecimal.ZERO;
|
||||
for (Map.Entry<Long, BigDecimal> e : hoursByTask.entrySet()) {
|
||||
ProjectTaskDO task = taskMap.get(e.getKey());
|
||||
for (Map.Entry<Long, BigDecimal> entry : hoursByTask.entrySet()) {
|
||||
ProjectTaskDO task = taskMap.get(entry.getKey());
|
||||
if (task != null) {
|
||||
hoursByProject.merge(task.getProjectId(), e.getValue(), BigDecimal::add);
|
||||
} else if (personalHit.contains(e.getKey())) {
|
||||
personalHours = personalHours.add(e.getValue());
|
||||
hoursByProject.merge(task.getProjectId(), entry.getValue(), BigDecimal::add);
|
||||
} else if (personalHit.contains(entry.getKey())) {
|
||||
personalHours = personalHours.add(entry.getValue());
|
||||
} else {
|
||||
otherHours = otherHours.add(e.getValue());
|
||||
otherHours = otherHours.add(entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
Map<Long, ProjectDO> projectMap = hoursByProject.isEmpty() ? Collections.emptyMap()
|
||||
: projectMapper.selectBatchIds(hoursByProject.keySet()).stream()
|
||||
.collect(Collectors.toMap(ProjectDO::getId, Function.identity(), (a, b) -> a));
|
||||
.collect(Collectors.toMap(ProjectDO::getId, Function.identity(), (a, b) -> a));
|
||||
|
||||
List<MyWorklogWeekRespVO.DistributionItemVO> items = new ArrayList<>();
|
||||
hoursByProject.entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
|
||||
.forEach(e -> {
|
||||
.forEach(entry -> {
|
||||
MyWorklogWeekRespVO.DistributionItemVO item = new MyWorklogWeekRespVO.DistributionItemVO();
|
||||
item.setProjectId(e.getKey());
|
||||
ProjectDO project = projectMap.get(e.getKey());
|
||||
item.setProjectId(entry.getKey());
|
||||
ProjectDO project = projectMap.get(entry.getKey());
|
||||
item.setProjectName(project == null ? null : project.getProjectName());
|
||||
item.setKind("project");
|
||||
item.setHours(e.getValue().setScale(2, RoundingMode.HALF_UP));
|
||||
item.setHours(entry.getValue().setScale(2, RoundingMode.HALF_UP));
|
||||
items.add(item);
|
||||
});
|
||||
if (personalHours.signum() > 0) {
|
||||
|
||||
@@ -10,8 +10,8 @@ import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工时周聚合的纯函数支撑:周一归一 + 按段均摊。
|
||||
* 口径(2026-06-12 与用户确认):工时按段填报无逐日明细,均摊分母 = 段内工作日(周一~周五)数;
|
||||
* 纯周末段全额兜底归段结束日前最近的工作日;份额 scale=4,最终展示由调用方聚合后 setScale(2)。
|
||||
* 口径(2026-07-14 更新):工时按段填报无逐日明细,均摊分母 = 段内工作日(周一~周五)数;
|
||||
* 周六/周日不参与工作日段均摊;周末单天工时保留在当天展示;份额 scale=4,最终展示由调用方聚合后 setScale(2)。
|
||||
*/
|
||||
final class MyWorklogWeekSupport {
|
||||
|
||||
@@ -25,7 +25,7 @@ final class MyWorklogWeekSupport {
|
||||
|
||||
/**
|
||||
* 把一笔按段填报的工时摊到所选周的工作日。
|
||||
* 返回 [weekMonday, weekMonday+4] 内各工作日的份额(scale=4);无份额的日不出现在 map。
|
||||
* 返回 [weekMonday, weekMonday+6] 内各自然日的份额(scale=4);无份额的日不出现在 map。
|
||||
*
|
||||
* <p>前置条件:weekMonday 必须已归一到 ISO 周的周一(调用方先调 {@link #normalizeToMonday})。
|
||||
* 方法内不做防御归一,以便在调用方传入非周一时尽早暴露 bug。
|
||||
@@ -36,26 +36,30 @@ final class MyWorklogWeekSupport {
|
||||
if (segStart == null || segEnd == null || hours == null || segStart.isAfter(segEnd)) {
|
||||
return result;
|
||||
}
|
||||
LocalDate weekFriday = weekMonday.plusDays(4);
|
||||
LocalDate weekSunday = weekMonday.plusDays(6);
|
||||
List<LocalDate> workdays = segStart.datesUntil(segEnd.plusDays(1))
|
||||
.filter(d -> d.getDayOfWeek().getValue() <= 5)
|
||||
.toList();
|
||||
if (workdays.isEmpty()) {
|
||||
// 纯周末段:全额归段结束日前最近的工作日(周六/周日 → 周五)
|
||||
LocalDate fallback = segEnd;
|
||||
while (fallback.getDayOfWeek().getValue() > 5) {
|
||||
fallback = fallback.minusDays(1);
|
||||
}
|
||||
if (!fallback.isBefore(weekMonday) && !fallback.isAfter(weekFriday)) {
|
||||
result.put(fallback, hours.setScale(4, RoundingMode.HALF_UP));
|
||||
if (!workdays.isEmpty()) {
|
||||
BigDecimal share = hours.divide(BigDecimal.valueOf(workdays.size()), 4, RoundingMode.HALF_UP);
|
||||
for (LocalDate d : workdays) {
|
||||
if (!d.isBefore(weekMonday) && !d.isAfter(weekSunday)) {
|
||||
result.put(d, share);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
BigDecimal share = hours.divide(BigDecimal.valueOf(workdays.size()), 4, RoundingMode.HALF_UP);
|
||||
for (LocalDate d : workdays) {
|
||||
if (!d.isBefore(weekMonday) && !d.isAfter(weekFriday)) {
|
||||
result.put(d, share);
|
||||
}
|
||||
|
||||
List<LocalDate> weekendDays = segStart.datesUntil(segEnd.plusDays(1))
|
||||
.filter(d -> d.getDayOfWeek().getValue() > 5)
|
||||
.filter(d -> !d.isBefore(weekMonday) && !d.isAfter(weekSunday))
|
||||
.toList();
|
||||
if (weekendDays.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
BigDecimal share = hours.divide(BigDecimal.valueOf(weekendDays.size()), 4, RoundingMode.HALF_UP);
|
||||
for (LocalDate d : weekendDays) {
|
||||
result.put(d, share);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
|
||||
import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants;
|
||||
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.service.ProjectObjectAuthorizationService;
|
||||
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.task.ProjectTaskService;
|
||||
@@ -128,6 +129,8 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
||||
@Resource
|
||||
private ProjectRequirementMapper projectRequirementMapper;
|
||||
@Resource
|
||||
private ProjectObjectAuthorizationService projectObjectAuthorizationService;
|
||||
@Resource
|
||||
private StatusActionTextResolver statusActionTextResolver;
|
||||
/** 站内信事件发布:创建执行后发「执行指派」通知,由 NotifySendEventListener 事务提交后统一发送。 */
|
||||
@Resource
|
||||
@@ -201,16 +204,16 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
||||
NotifyMessageLevelConstants.NORMAL, SecurityFrameworkUtils.getLoginUserId()));
|
||||
}
|
||||
|
||||
// 权限:方法体内属主放行 + 权限码兜底(requireExecutionEditDeletePermission),勿挂 @CheckObjectPermission
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||
permission = ProjectExecutionConstants.PERMISSION_UPDATE)
|
||||
public void updateExecution(Long projectId, ProjectExecutionUpdateReqVO reqVO) {
|
||||
if (reqVO.getId() == null) {
|
||||
throw invalidParamException("执行编号不能为空");
|
||||
}
|
||||
validateEditableProject(projectId);
|
||||
ProjectDO project = validateEditableProject(projectId);
|
||||
ProjectExecutionDO execution = validateExecutionExists(projectId, reqVO.getId());
|
||||
requireExecutionEditDeletePermission(project, execution, ProjectExecutionConstants.PERMISSION_UPDATE);
|
||||
validateExecutionAllowEdit(execution);
|
||||
projectRequirementService.validateUsableForExecution(projectId, reqVO.getProjectRequirementId());
|
||||
String executionName = normalizeRequiredName(reqVO.getExecutionName());
|
||||
@@ -448,13 +451,13 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
||||
ObjectActivityConstants.EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_IN, reason);
|
||||
}
|
||||
|
||||
// 权限:方法体内属主放行 + 权限码兜底(requireExecutionEditDeletePermission),勿挂 @CheckObjectPermission
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||
permission = ProjectExecutionConstants.PERMISSION_DELETE)
|
||||
public void deleteExecution(Long projectId, Long executionId, ProjectExecutionDeleteReqVO reqVO) {
|
||||
validateProjectExists(projectId);
|
||||
ProjectDO project = validateProjectExists(projectId);
|
||||
ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
|
||||
requireExecutionEditDeletePermission(project, execution, ProjectExecutionConstants.PERMISSION_DELETE);
|
||||
validateDeleteConfirmText(reqVO.getConfirmText());
|
||||
if (!Objects.equals(execution.getExecutionName(), reqVO.getExecutionName().trim())) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_EXECUTION_DELETE_NAME_MISMATCH);
|
||||
@@ -482,12 +485,12 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
||||
writeExecutionAuditLog(execution, ObjectActivityConstants.EXECUTION_ACTION_DELETE, fromStatus, null, null, reason);
|
||||
}
|
||||
|
||||
// 权限:方法体内属主放行 + 权限码兜底(requireExecutionEditDeletePermission),勿挂 @CheckObjectPermission
|
||||
@Override
|
||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||
permission = ProjectExecutionConstants.PERMISSION_DELETE)
|
||||
public ProjectExecutionDeletePrecheckRespVO precheckDeleteExecution(Long projectId, Long executionId) {
|
||||
validateProjectExists(projectId);
|
||||
validateExecutionExists(projectId, executionId);
|
||||
ProjectDO project = validateProjectExists(projectId);
|
||||
ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
|
||||
requireExecutionEditDeletePermission(project, execution, ProjectExecutionConstants.PERMISSION_DELETE);
|
||||
int taskCount = projectTaskMapper.countByExecutionId(executionId);
|
||||
ProjectExecutionDeletePrecheckRespVO respVO = new ProjectExecutionDeletePrecheckRespVO();
|
||||
respVO.setTaskCount(taskCount);
|
||||
@@ -495,6 +498,27 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
||||
return respVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行编辑/删除授权:项目负责人、执行负责人直接放行;其它用户回落对象域权限码。
|
||||
*
|
||||
* <p>红线:调用本方法的入口(updateExecution / deleteExecution / precheckDeleteExecution)<b>故意不挂
|
||||
* {@code @CheckObjectPermission}</b>——注解是前置拦截,会把"无权限码的负责人"挡死。
|
||||
* 不要误判为漏鉴权把注解补回去;口径见 CLAUDE.md §鉴权。
|
||||
*/
|
||||
private void requireExecutionEditDeletePermission(ProjectDO project, ProjectExecutionDO execution,
|
||||
String permission) {
|
||||
List<Long> ownerUserIds = new ArrayList<>();
|
||||
addIfNotNull(ownerUserIds, project.getManagerUserId());
|
||||
addIfNotNull(ownerUserIds, execution.getOwnerId());
|
||||
projectObjectAuthorizationService.checkAnyOwnerOrProjectPermission(project.getId(), ownerUserIds, permission);
|
||||
}
|
||||
|
||||
private void addIfNotNull(Collection<Long> values, Long value) {
|
||||
if (value != null) {
|
||||
values.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDeleteConfirmText(String confirmText) {
|
||||
String normalizedConfirmText = normalizeNullableText(confirmText);
|
||||
if (normalizedConfirmText == null
|
||||
|
||||
@@ -55,11 +55,11 @@ public interface ProjectTaskService {
|
||||
void changeTaskStatus(Long projectId, Long executionId, Long taskId, ProjectTaskStatusActionReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 删除任务(软删 + 三重确认 + 上级硬卡 + 级联软删 + 父进度回算 + 审计)。
|
||||
* 删除任务(软删 + 三重确认 + 多身份授权 + 级联软删 + 父进度回算 + 审计)。
|
||||
* <p>删除规则:非 completed 即可删(pending / active / paused / cancelled 全允许);
|
||||
* 已 completed 的任务用专门错误码 PROJECT_TASK_NOT_ALLOW_DELETE 拦截。
|
||||
* <p>上级硬卡:一级任务由"执行负责人 OR 项目负责人(权限码 {@code project:task:delete})"删;
|
||||
* 子任务由"父任务负责人 OR 项目负责人(权限码)"删。
|
||||
* <p>授权:项目负责人 / 执行负责人 / 任务负责人 / 父任务负责人均可删;
|
||||
* 其它用户持有 {@code project:task:delete} 时作为管理员兜底放行。
|
||||
* <p>三重确认:{@code taskName} / {@code confirmText}(DELETE 或 删除)/ {@code reason} 全部必填,
|
||||
* 任意一项缺失或不匹配即拒。
|
||||
* <p>级联范围:软删该任务及其所有子孙任务、所有相关工作日志、所有相关任务协办;
|
||||
|
||||
@@ -135,6 +135,7 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
||||
@Resource
|
||||
private org.springframework.context.ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
// 权限:方法体内属主放行 + 权限码兜底(checkOwnerOrProjectPermission),勿挂 @CheckObjectPermission
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createTask(Long projectId, Long executionId, ProjectTaskSaveReqVO reqVO) {
|
||||
@@ -215,24 +216,19 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
||||
NotifyMessageLevelConstants.NORMAL, SecurityFrameworkUtils.getLoginUserId()));
|
||||
}
|
||||
|
||||
// 权限:方法体内属主放行 + 权限码兜底(requireTaskEditDeletePermission),勿挂 @CheckObjectPermission
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateTask(Long projectId, Long executionId, ProjectTaskSaveReqVO reqVO) {
|
||||
if (reqVO.getId() == null) {
|
||||
throw invalidParamException("任务编号不能为空");
|
||||
}
|
||||
validateEditableProject(projectId);
|
||||
ProjectDO project = validateEditableProject(projectId);
|
||||
ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
|
||||
validateExecutionAllowEdit(execution);
|
||||
ProjectTaskDO task = validateTaskExists(projectId, executionId, reqVO.getId());
|
||||
validateTaskAllowEdit(task);
|
||||
// 上级硬卡:一级任务由"执行负责人 OR 项目负责人(权限码)"改;子任务由"父任务负责人 OR 项目负责人(权限码)"改
|
||||
// 按当前 task 的 parentTaskId(修改前)判定;改父亲属于改动结果,授权按改动前形态裁决
|
||||
Long upperOwnerId = task.getParentTaskId() == null
|
||||
? execution.getOwnerId()
|
||||
: projectTaskMapper.selectById(task.getParentTaskId()).getOwnerId();
|
||||
projectObjectAuthorizationService.checkOwnerOrProjectPermission(
|
||||
projectId, upperOwnerId, ProjectTaskConstants.PERMISSION_UPDATE);
|
||||
requireTaskEditDeletePermission(project, execution, task, ProjectTaskConstants.PERMISSION_UPDATE);
|
||||
ProjectTaskDO parentTask = validateParentTask(projectId, executionId, reqVO.getParentTaskId());
|
||||
if (parentTask != null && Objects.equals(parentTask.getId(), task.getId())) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_TASK_PARENT_INVALID);
|
||||
@@ -281,14 +277,14 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
||||
task.getStatusCode(), buildTaskFieldChanges(before, task), null);
|
||||
}
|
||||
|
||||
// 权限:方法体内属主放行 + 权限码兜底(requireTaskEditDeletePermission),勿挂 @CheckObjectPermission
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteTask(Long projectId, Long executionId, Long taskId, ProjectTaskDeleteReqVO reqVO) {
|
||||
validateProjectExists(projectId);
|
||||
ProjectDO project = validateProjectExists(projectId);
|
||||
ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
|
||||
ProjectTaskDO task = validateTaskExists(projectId, executionId, taskId);
|
||||
// 上级硬卡:一级任务由"执行负责人 OR 项目负责人(权限码)"删;子任务由"父任务负责人 OR 项目负责人(权限码)"删
|
||||
requireDeletePermissionOnTask(projectId, task, execution);
|
||||
requireTaskEditDeletePermission(project, execution, task, ProjectTaskConstants.PERMISSION_DELETE);
|
||||
validateDeleteConfirmText(reqVO.getConfirmText());
|
||||
if (!Objects.equals(task.getTaskTitle(), reqVO.getTaskName().trim())) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_TASK_DELETE_NAME_MISMATCH);
|
||||
@@ -326,13 +322,14 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
||||
writeTaskAuditLog(task, ObjectActivityConstants.TASK_ACTION_DELETE, fromStatus, null, null, reason);
|
||||
}
|
||||
|
||||
// 权限:方法体内属主放行 + 权限码兜底(requireTaskEditDeletePermission),勿挂 @CheckObjectPermission
|
||||
@Override
|
||||
public ProjectTaskDeletePrecheckRespVO precheckDeleteTask(Long projectId, Long executionId, Long taskId) {
|
||||
validateProjectExists(projectId);
|
||||
ProjectDO project = validateProjectExists(projectId);
|
||||
ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
|
||||
ProjectTaskDO task = validateTaskExists(projectId, executionId, taskId);
|
||||
// 预检也走"上级硬卡",防止"无权限的人调 precheck 试探下挂数量"
|
||||
requireDeletePermissionOnTask(projectId, task, execution);
|
||||
// 预检也走同款编辑/删除授权,防止无权限用户调 precheck 试探下挂数量。
|
||||
requireTaskEditDeletePermission(project, execution, task, ProjectTaskConstants.PERMISSION_DELETE);
|
||||
|
||||
int childTaskCount = projectTaskMapper.countChildrenByParentTaskId(taskId);
|
||||
int worklogCount = taskWorklogMapper.countByTaskId(taskId);
|
||||
@@ -345,26 +342,32 @@ public class ProjectTaskServiceImpl implements ProjectTaskService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务删除"上级硬卡":一级任务由执行负责人 OR 项目负责人(权限码)操作;
|
||||
* 子任务由父任务负责人 OR 项目负责人(权限码)操作。
|
||||
* <p>同时被 deleteTask 与 precheckDeleteTask 使用,保证删除路径与预检入口走同款权限通道。
|
||||
* 任务编辑/删除授权:项目负责人、执行负责人、任务负责人、父任务负责人均可操作;
|
||||
* 其它用户回落到项目对象角色权限码,作为管理员兜底。
|
||||
*
|
||||
* <p>红线:调用本方法的入口(updateTask / deleteTask / precheckDeleteTask)<b>故意不挂
|
||||
* {@code @CheckObjectPermission}</b>——注解是前置拦截,会把"无权限码的负责人"挡死。
|
||||
* 不要误判为漏鉴权把注解补回去;口径见 CLAUDE.md §鉴权。
|
||||
*/
|
||||
private void requireDeletePermissionOnTask(Long projectId, ProjectTaskDO task, ProjectExecutionDO execution) {
|
||||
Long upperOwnerId;
|
||||
if (task.getParentTaskId() == null) {
|
||||
upperOwnerId = execution.getOwnerId();
|
||||
} else {
|
||||
private void requireTaskEditDeletePermission(ProjectDO project, ProjectExecutionDO execution,
|
||||
ProjectTaskDO task, String permission) {
|
||||
List<Long> ownerUserIds = new ArrayList<>();
|
||||
addIfNotNull(ownerUserIds, project.getManagerUserId());
|
||||
addIfNotNull(ownerUserIds, execution.getOwnerId());
|
||||
addIfNotNull(ownerUserIds, task.getOwnerId());
|
||||
if (task.getParentTaskId() != null) {
|
||||
ProjectTaskDO parent = projectTaskMapper.selectById(task.getParentTaskId());
|
||||
if (parent == null) {
|
||||
// 父任务被并发硬删的极端场景:当前任务已是孤儿,按"无上级负责人"处理
|
||||
// 仍要求调用方持有项目级权限码,由 checkOwnerOrProjectPermission 兜底拦截
|
||||
upperOwnerId = null;
|
||||
} else {
|
||||
upperOwnerId = parent.getOwnerId();
|
||||
if (parent != null) {
|
||||
addIfNotNull(ownerUserIds, parent.getOwnerId());
|
||||
}
|
||||
}
|
||||
projectObjectAuthorizationService.checkOwnerOrProjectPermission(
|
||||
projectId, upperOwnerId, ProjectTaskConstants.PERMISSION_DELETE);
|
||||
projectObjectAuthorizationService.checkAnyOwnerOrProjectPermission(project.getId(), ownerUserIds, permission);
|
||||
}
|
||||
|
||||
private void addIfNotNull(Collection<Long> values, Long value) {
|
||||
if (value != null) {
|
||||
values.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDeleteConfirmText(String confirmText) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.njcn.rdms.module.project.service.signoff;
|
||||
|
||||
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
|
||||
|
||||
public interface EmployeeSignOffStatusService {
|
||||
|
||||
String getInitialStatusCode();
|
||||
|
||||
String statusName(String statusCode);
|
||||
|
||||
ObjectStatusTransitionDO validateTransition(String fromStatus, String actionCode, String reason);
|
||||
|
||||
ObjectStatusModelDO validateStatusModel(String statusCode);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.njcn.rdms.module.project.service.signoff;
|
||||
|
||||
import com.njcn.rdms.module.project.constant.SignOffConstants;
|
||||
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.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.project.service.status.StatusActionTextResolver;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
|
||||
@Service
|
||||
public class EmployeeSignOffStatusServiceImpl implements EmployeeSignOffStatusService {
|
||||
|
||||
@Resource
|
||||
private ObjectStatusModelMapper objectStatusModelMapper;
|
||||
@Resource
|
||||
private ObjectStatusTransitionMapper objectStatusTransitionMapper;
|
||||
@Resource
|
||||
private StatusActionTextResolver statusActionTextResolver;
|
||||
|
||||
@Override
|
||||
public String getInitialStatusCode() {
|
||||
ObjectStatusModelDO model = objectStatusModelMapper.selectInitialByObjectTypeEnabled(SignOffConstants.STATUS_OBJECT_TYPE);
|
||||
if (model == null || !StringUtils.hasText(model.getStatusCode())) {
|
||||
throw exception(ErrorCodeConstants.SIGN_OFF_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
||||
}
|
||||
return model.getStatusCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String statusName(String statusCode) {
|
||||
return statusActionTextResolver.statusName(SignOffConstants.STATUS_OBJECT_TYPE, statusCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectStatusTransitionDO validateTransition(String fromStatus, String actionCode, String reason) {
|
||||
ObjectStatusTransitionDO transition = objectStatusTransitionMapper.selectByObjectTypeAndFromStatusAndAction(
|
||||
SignOffConstants.STATUS_OBJECT_TYPE, fromStatus, actionCode);
|
||||
if (transition == null) {
|
||||
throw exception(ErrorCodeConstants.SIGN_OFF_STATUS_ACTION_NOT_ALLOWED,
|
||||
statusActionTextResolver.statusName(SignOffConstants.STATUS_OBJECT_TYPE, fromStatus),
|
||||
statusActionTextResolver.actionName(SignOffConstants.STATUS_OBJECT_TYPE, actionCode));
|
||||
}
|
||||
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||
throw exception(ErrorCodeConstants.SIGN_OFF_STATUS_ACTION_REASON_REQUIRED,
|
||||
statusActionTextResolver.actionName(SignOffConstants.STATUS_OBJECT_TYPE, actionCode));
|
||||
}
|
||||
validateStatusModel(transition.getToStatusCode());
|
||||
return transition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectStatusModelDO validateStatusModel(String statusCode) {
|
||||
ObjectStatusModelDO model = objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
|
||||
SignOffConstants.STATUS_OBJECT_TYPE, statusCode);
|
||||
if (model == null) {
|
||||
throw exception(ErrorCodeConstants.SIGN_OFF_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
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.ProjectObjectConstants;
|
||||
import com.njcn.rdms.module.project.constant.SignOffConstants;
|
||||
import com.njcn.rdms.module.project.constant.WorkReportConstants;
|
||||
import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
|
||||
import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants;
|
||||
@@ -24,6 +25,8 @@ import com.njcn.rdms.module.project.dal.dataobject.workreport.common.WorkReportM
|
||||
import com.njcn.rdms.module.project.dal.dataobject.workreport.common.WorkReportStatusLogDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportApprovalRecordDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportSignOffDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportSignOffRecordDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportApprovalRecordDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportCurrentItemDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportDO;
|
||||
@@ -41,6 +44,8 @@ import com.njcn.rdms.module.project.dal.mysql.workreport.common.PersonalReportRe
|
||||
import com.njcn.rdms.module.project.dal.mysql.workreport.common.WorkReportStatusLogMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.workreport.monthly.MonthlyReportApprovalRecordMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.workreport.monthly.MonthlyReportMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.workreport.monthly.MonthlyReportSignOffMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.workreport.monthly.MonthlyReportSignOffRecordMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportApprovalRecordMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportCurrentItemMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportMapper;
|
||||
@@ -49,10 +54,12 @@ import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportAppr
|
||||
import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportTravelSegmentMapper;
|
||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||
import com.njcn.rdms.module.project.service.signoff.EmployeeSignOffStatusService;
|
||||
import com.njcn.rdms.module.project.service.status.StatusActionTextResolver;
|
||||
import com.njcn.rdms.module.project.service.team.TeamDashboardAccessService;
|
||||
import com.njcn.rdms.module.system.api.dept.DeptApi;
|
||||
import com.njcn.rdms.module.system.api.dept.PostApi;
|
||||
import com.njcn.rdms.module.system.api.user.UserSignatureApi;
|
||||
import com.njcn.rdms.module.system.api.dept.dto.DeptRespDTO;
|
||||
import com.njcn.rdms.module.system.api.dept.dto.PostRespDTO;
|
||||
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
|
||||
@@ -60,6 +67,7 @@ import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO;
|
||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||
import com.njcn.rdms.module.system.api.user.UserManagementRelationApi;
|
||||
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||
import com.njcn.rdms.module.system.api.user.dto.UserSignatureRespDTO;
|
||||
import com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
@@ -109,6 +117,10 @@ public class WorkReportCommonService {
|
||||
@Resource
|
||||
private MonthlyReportApprovalRecordMapper monthlyReportApprovalRecordMapper;
|
||||
@Resource
|
||||
private MonthlyReportSignOffMapper monthlyReportSignOffMapper;
|
||||
@Resource
|
||||
private MonthlyReportSignOffRecordMapper monthlyReportSignOffRecordMapper;
|
||||
@Resource
|
||||
private ProjectReportApprovalRecordMapper projectReportApprovalRecordMapper;
|
||||
@Resource
|
||||
private WorkReportStatusLogMapper workReportStatusLogMapper;
|
||||
@@ -121,6 +133,8 @@ public class WorkReportCommonService {
|
||||
@Resource
|
||||
private StatusActionTextResolver statusActionTextResolver;
|
||||
@Resource
|
||||
private EmployeeSignOffStatusService employeeSignOffStatusService;
|
||||
@Resource
|
||||
private AdminUserApi adminUserApi;
|
||||
@Resource
|
||||
private DeptApi deptApi;
|
||||
@@ -129,6 +143,8 @@ public class WorkReportCommonService {
|
||||
@Resource
|
||||
private UserManagementRelationApi userManagementRelationApi;
|
||||
@Resource
|
||||
private UserSignatureApi userSignatureApi;
|
||||
@Resource
|
||||
private ObjectPermissionApi objectPermissionApi;
|
||||
@Resource
|
||||
private ProjectMapper projectMapper;
|
||||
@@ -368,6 +384,7 @@ public class WorkReportCommonService {
|
||||
MonthlyReportDO report = new MonthlyReportDO();
|
||||
applyMonthlySaveFields(report, reqVO, profile);
|
||||
report.setStatusCode(getInitialStatusCode());
|
||||
report.setSignOffStatus(SignOffConstants.STATUS_NOT_STARTED);
|
||||
monthlyReportMapper.insert(report);
|
||||
replaceMonthlyChildren(report.getId(), reqVO);
|
||||
MonthlyReportDO latest = monthlyReportMapper.selectById(report.getId());
|
||||
@@ -439,6 +456,29 @@ public class WorkReportCommonService {
|
||||
.collect(Collectors.toList()), pageResult.getTotal());
|
||||
}
|
||||
|
||||
public PageResult<MonthlyReportRespVO> getMonthlySignOffPage(MonthlyReportPageReqVO reqVO) {
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
reqVO.setStatusCode(WorkReportConstants.STATUS_APPROVED);
|
||||
reqVO.setSignOffStatus(SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM);
|
||||
PageResult<MonthlyReportDO> pageResult = monthlyReportMapper.selectReporterPage(loginUserId, reqVO,
|
||||
List.of(WorkReportConstants.STATUS_APPROVED));
|
||||
return new PageResult<>(pageResult.getList().stream()
|
||||
.map(report -> toMonthlyRespVO(report, false))
|
||||
.collect(Collectors.toList()), pageResult.getTotal());
|
||||
}
|
||||
|
||||
public MonthlyReportSignOffDetailRespVO getMonthlySignOffDetail(Long id) {
|
||||
MonthlyReportDO report = validateMonthlyReportExists(id);
|
||||
validateReporter(SecurityFrameworkUtils.getLoginUserId(), report.getReporterId());
|
||||
MonthlyReportSignOffDetailRespVO respVO = new MonthlyReportSignOffDetailRespVO();
|
||||
respVO.setReport(toMonthlyRespVO(report, true));
|
||||
MonthlyReportApprovalRecordDO latestApprovalRecord = monthlyReportApprovalRecordMapper
|
||||
.selectLatestApprovedByMonthlyReportId(id);
|
||||
respVO.setLatestApprovalRecord(latestApprovalRecord == null ? null
|
||||
: BeanUtils.toBean(latestApprovalRecord, MonthlyReportApprovalRecordRespVO.class));
|
||||
return respVO;
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void submitMonthlyReport(Long id) {
|
||||
MonthlyReportDO current = validateMonthlyReportExists(id);
|
||||
@@ -478,6 +518,55 @@ public class WorkReportCommonService {
|
||||
processMonthlyApproval(id, WorkReportConstants.ACTION_REJECT, rejectReqVO);
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void confirmMonthlySignOff(Long id, MonthlyReportSignOffConfirmReqVO reqVO) {
|
||||
MonthlyReportDO current = validateMonthlyReportExists(id);
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
validateReporter(loginUserId, current.getReporterId());
|
||||
if (!WorkReportConstants.STATUS_APPROVED.equals(current.getStatusCode())
|
||||
|| !SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM.equals(current.getSignOffStatus())) {
|
||||
throw exception(ErrorCodeConstants.WORK_REPORT_SIGN_OFF_NOT_ALLOWED);
|
||||
}
|
||||
MonthlyReportSignOffDO signOff = validateMonthlySignOffExists(current.getId());
|
||||
employeeSignOffStatusService.validateTransition(signOff.getStatusCode(), SignOffConstants.ACTION_CONFIRM, null);
|
||||
MonthlyReportApprovalRecordDO approvalRecord = monthlyReportApprovalRecordMapper
|
||||
.selectLatestApprovedByMonthlyReportId(current.getId());
|
||||
if (approvalRecord == null) {
|
||||
throw exception(ErrorCodeConstants.WORK_REPORT_MONTHLY_SIGN_OFF_APPROVAL_RECORD_NOT_EXISTS);
|
||||
}
|
||||
|
||||
UserSignatureRespDTO userSignature = getUserSignature(loginUserId);
|
||||
LocalDate signedDate = reqVO.getEmployeeSignedDate() != null ? reqVO.getEmployeeSignedDate() : LocalDate.now();
|
||||
MonthlyReportApprovalRecordDO approvalUpdate = new MonthlyReportApprovalRecordDO();
|
||||
approvalUpdate.setId(approvalRecord.getId());
|
||||
approvalUpdate.setMeetingDate(reqVO.getMeetingDate());
|
||||
approvalUpdate.setEmployeeSignName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||
approvalUpdate.setEmployeeSignatureFileId(userSignature == null ? null : userSignature.getFileId());
|
||||
approvalUpdate.setEmployeeSignedDate(signedDate);
|
||||
monthlyReportApprovalRecordMapper.updateById(approvalUpdate);
|
||||
|
||||
MonthlyReportDO reportUpdate = new MonthlyReportDO();
|
||||
reportUpdate.setSignOffStatus(SignOffConstants.STATUS_EMPLOYEE_CONFIRMED);
|
||||
int reportUpdateCount = monthlyReportMapper.updateByIdAndStatusAndSignOffStatus(reportUpdate, current.getId(),
|
||||
current.getStatusCode(), current.getSignOffStatus());
|
||||
if (reportUpdateCount != 1) {
|
||||
throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED);
|
||||
}
|
||||
|
||||
MonthlyReportSignOffDO signOffUpdate = new MonthlyReportSignOffDO();
|
||||
signOffUpdate.setStatusCode(SignOffConstants.STATUS_EMPLOYEE_CONFIRMED);
|
||||
signOffUpdate.setConfirmUserId(loginUserId);
|
||||
signOffUpdate.setConfirmUserName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||
signOffUpdate.setConfirmTime(LocalDateTime.now());
|
||||
int signOffUpdateCount = monthlyReportSignOffMapper.updateByMonthlyReportIdAndStatus(signOffUpdate,
|
||||
current.getId(), signOff.getStatusCode());
|
||||
if (signOffUpdateCount != 1) {
|
||||
throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED);
|
||||
}
|
||||
writeMonthlySignOffRecord(signOff.getId(), current.getId(), approvalRecord.getId(), signOff.getBatchNo(),
|
||||
SignOffConstants.ACTION_CONFIRM, signOff.getStatusCode(), SignOffConstants.STATUS_EMPLOYEE_CONFIRMED);
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteMonthlyReport(Long id) {
|
||||
MonthlyReportDO current = validateMonthlyReportExists(id);
|
||||
@@ -513,6 +602,89 @@ public class WorkReportCommonService {
|
||||
return BeanUtils.toBean(getMonthlyReportPage(reqVO).getList(), MonthlyReportExportVO.class);
|
||||
}
|
||||
|
||||
public List<MonthlyReportDO> listMonthlyReportsForSignOff(Collection<Long> reporterIds, String periodMonth) {
|
||||
return monthlyReportMapper.selectListByReporterIdsAndPeriodKey(reporterIds, periodMonth,
|
||||
List.of(WorkReportConstants.STATUS_APPROVED));
|
||||
}
|
||||
|
||||
public void issueMonthlySignOff(List<MonthlyReportDO> reports, Long batchNo) {
|
||||
if (reports == null || reports.isEmpty()) {
|
||||
throw exception(ErrorCodeConstants.WORK_REPORT_MONTHLY_SIGN_OFF_CANDIDATE_EMPTY);
|
||||
}
|
||||
Long operatorUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
String operatorName = defaultText(SecurityFrameworkUtils.getLoginUserNickname());
|
||||
LocalDateTime issueTime = LocalDateTime.now();
|
||||
for (MonthlyReportDO report : reports) {
|
||||
if (!WorkReportConstants.STATUS_APPROVED.equals(report.getStatusCode())
|
||||
|| !isIssueAllowed(report.getSignOffStatus())) {
|
||||
throw exception(ErrorCodeConstants.WORK_REPORT_SIGN_OFF_NOT_ALLOWED);
|
||||
}
|
||||
MonthlyReportApprovalRecordDO approvalRecord = monthlyReportApprovalRecordMapper
|
||||
.selectLatestApprovedByMonthlyReportId(report.getId());
|
||||
if (approvalRecord == null) {
|
||||
throw exception(ErrorCodeConstants.WORK_REPORT_MONTHLY_SIGN_OFF_APPROVAL_RECORD_NOT_EXISTS);
|
||||
}
|
||||
|
||||
MonthlyReportDO reportUpdate = new MonthlyReportDO();
|
||||
reportUpdate.setSignOffStatus(SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM);
|
||||
int reportUpdateCount = monthlyReportMapper.updateByIdAndStatusAndSignOffStatus(reportUpdate, report.getId(),
|
||||
report.getStatusCode(), report.getSignOffStatus());
|
||||
if (reportUpdateCount != 1) {
|
||||
throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED);
|
||||
}
|
||||
|
||||
MonthlyReportSignOffDO currentSignOff = monthlyReportSignOffMapper.selectByMonthlyReportId(report.getId());
|
||||
if (currentSignOff == null) {
|
||||
MonthlyReportSignOffDO signOff = new MonthlyReportSignOffDO();
|
||||
signOff.setMonthlyReportId(report.getId());
|
||||
signOff.setBatchNo(batchNo);
|
||||
signOff.setStatusCode(SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM);
|
||||
signOff.setIssueUserId(operatorUserId);
|
||||
signOff.setIssueUserName(operatorName);
|
||||
signOff.setIssueTime(issueTime);
|
||||
monthlyReportSignOffMapper.insert(signOff);
|
||||
writeMonthlySignOffRecord(signOff.getId(), report.getId(), approvalRecord.getId(), batchNo,
|
||||
SignOffConstants.ACTION_ISSUE, SignOffConstants.STATUS_NOT_STARTED,
|
||||
SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM);
|
||||
} else {
|
||||
employeeSignOffStatusService.validateTransition(currentSignOff.getStatusCode(), SignOffConstants.ACTION_ISSUE, null);
|
||||
MonthlyReportSignOffDO signOffUpdate = new MonthlyReportSignOffDO();
|
||||
signOffUpdate.setStatusCode(SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM);
|
||||
signOffUpdate.setBatchNo(batchNo);
|
||||
signOffUpdate.setIssueUserId(operatorUserId);
|
||||
signOffUpdate.setIssueUserName(operatorName);
|
||||
signOffUpdate.setIssueTime(issueTime);
|
||||
signOffUpdate.setConfirmUserId(null);
|
||||
signOffUpdate.setConfirmUserName(null);
|
||||
signOffUpdate.setConfirmTime(null);
|
||||
int signOffUpdateCount = monthlyReportSignOffMapper.updateByMonthlyReportIdAndStatus(signOffUpdate,
|
||||
report.getId(), currentSignOff.getStatusCode());
|
||||
if (signOffUpdateCount != 1) {
|
||||
throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED);
|
||||
}
|
||||
writeMonthlySignOffRecord(currentSignOff.getId(), report.getId(), approvalRecord.getId(), batchNo,
|
||||
SignOffConstants.ACTION_ISSUE, currentSignOff.getStatusCode(),
|
||||
SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void syncPerformanceResult(Long reporterId, String periodMonth, String performanceResult) {
|
||||
MonthlyReportDO report = monthlyReportMapper.selectByReporterIdAndPeriodKey(reporterId, periodMonth);
|
||||
if (report == null) {
|
||||
throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_MONTHLY_REPORT_NOT_EXISTS);
|
||||
}
|
||||
MonthlyReportApprovalRecordDO approvalRecord = monthlyReportApprovalRecordMapper
|
||||
.selectLatestApprovedByMonthlyReportId(report.getId());
|
||||
if (approvalRecord == null) {
|
||||
throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_MONTHLY_APPROVAL_RECORD_NOT_EXISTS);
|
||||
}
|
||||
MonthlyReportApprovalRecordDO update = new MonthlyReportApprovalRecordDO();
|
||||
update.setId(approvalRecord.getId());
|
||||
update.setPerformanceResult(performanceResult);
|
||||
monthlyReportApprovalRecordMapper.updateById(update);
|
||||
}
|
||||
|
||||
public ProjectReportRespVO initProjectReport(Long projectId) {
|
||||
CurrentUserProfile profile = loadCurrentUserProfile(true);
|
||||
ProjectDO project = validateProjectExists(projectId);
|
||||
@@ -810,6 +982,14 @@ public class WorkReportCommonService {
|
||||
return report;
|
||||
}
|
||||
|
||||
private MonthlyReportSignOffDO validateMonthlySignOffExists(Long monthlyReportId) {
|
||||
MonthlyReportSignOffDO signOff = monthlyReportSignOffMapper.selectByMonthlyReportId(monthlyReportId);
|
||||
if (signOff == null) {
|
||||
throw exception(ErrorCodeConstants.WORK_REPORT_MONTHLY_SIGN_OFF_NOT_EXISTS);
|
||||
}
|
||||
return signOff;
|
||||
}
|
||||
|
||||
private ProjectReportDO validateReadableProjectReport(Long id) {
|
||||
ProjectReportDO report = validateProjectReportExists(id);
|
||||
validateReadable(report.getProjectOwnerId(), report.getSupervisorUserId());
|
||||
@@ -1438,6 +1618,7 @@ public class WorkReportCommonService {
|
||||
record.setConclusion(statusLog.getToStatus());
|
||||
record.setOpinion(normalizeReason(reqVO));
|
||||
if (WorkReportConstants.ACTION_APPROVE.equals(actionCode)) {
|
||||
UserSignatureRespDTO userSignature = getUserSignature(SecurityFrameworkUtils.getLoginUserId());
|
||||
record.setMeetingDate(reqVO.getMeetingDate());
|
||||
record.setStrengthDesc(normalizeNullableText(reqVO.getStrengthDesc()));
|
||||
record.setStrengthExample(normalizeNullableText(reqVO.getStrengthExample()));
|
||||
@@ -1446,15 +1627,32 @@ public class WorkReportCommonService {
|
||||
record.setImprovementSuggestion(normalizeNullableText(reqVO.getImprovementSuggestion()));
|
||||
record.setPerformanceResult(normalizeNullableText(reqVO.getPerformanceResult()));
|
||||
record.setEmployeeSignName(normalizeNullableText(reqVO.getEmployeeSignName()));
|
||||
record.setEmployeeSignatureFileId(null);
|
||||
record.setEmployeeSignedDate(reqVO.getEmployeeSignedDate());
|
||||
record.setSupervisorSignName(normalizeNullableText(reqVO.getSupervisorSignName()));
|
||||
record.setSupervisorSignedDate(reqVO.getSupervisorSignedDate());
|
||||
record.setSupervisorSignName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||
record.setSupervisorSignatureFileId(userSignature == null ? null : userSignature.getFileId());
|
||||
record.setSupervisorSignedDate(LocalDate.now());
|
||||
}
|
||||
record.setAuditorUserId(SecurityFrameworkUtils.getLoginUserId());
|
||||
record.setAuditorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||
monthlyReportApprovalRecordMapper.insert(record);
|
||||
}
|
||||
|
||||
private void writeMonthlySignOffRecord(Long signOffId, Long monthlyReportId, Long approvalRecordId, Long batchNo,
|
||||
String actionType, String fromStatus, String toStatus) {
|
||||
MonthlyReportSignOffRecordDO record = new MonthlyReportSignOffRecordDO();
|
||||
record.setSignOffId(signOffId);
|
||||
record.setMonthlyReportId(monthlyReportId);
|
||||
record.setApprovalRecordId(approvalRecordId);
|
||||
record.setBatchNo(batchNo);
|
||||
record.setActionType(actionType);
|
||||
record.setFromSignOffStatus(fromStatus);
|
||||
record.setToSignOffStatus(toStatus);
|
||||
record.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
|
||||
record.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||
monthlyReportSignOffRecordMapper.insert(record);
|
||||
}
|
||||
|
||||
private void writeProjectApprovalRecord(ProjectReportDO report, WorkReportStatusLogDO statusLog, String reason) {
|
||||
ProjectReportApprovalRecordDO record = new ProjectReportApprovalRecordDO();
|
||||
record.setProjectReportId(report.getId());
|
||||
@@ -1513,6 +1711,11 @@ public class WorkReportCommonService {
|
||||
private MonthlyReportRespVO toMonthlyRespVO(MonthlyReportDO report, boolean withChildren) {
|
||||
MonthlyReportRespVO respVO = BeanUtils.toBean(report, MonthlyReportRespVO.class);
|
||||
applyStatusView(respVO, getStatusModel(report.getStatusCode()));
|
||||
String signOffStatus = StringUtils.hasText(report.getSignOffStatus())
|
||||
? report.getSignOffStatus()
|
||||
: SignOffConstants.STATUS_NOT_STARTED;
|
||||
respVO.setSignOffStatus(signOffStatus);
|
||||
respVO.setSignOffStatusName(employeeSignOffStatusService.statusName(signOffStatus));
|
||||
if (withChildren) {
|
||||
respVO.setReviewItems(BeanUtils.toBean(
|
||||
personalReportReviewItemMapper.selectListByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, report.getId()),
|
||||
@@ -1704,6 +1907,19 @@ public class WorkReportCommonService {
|
||||
return StringUtils.hasText(value) ? value.trim() : "";
|
||||
}
|
||||
|
||||
private boolean isIssueAllowed(String signOffStatus) {
|
||||
return !StringUtils.hasText(signOffStatus)
|
||||
|| SignOffConstants.STATUS_NOT_STARTED.equals(signOffStatus);
|
||||
}
|
||||
|
||||
private UserSignatureRespDTO getUserSignature(Long userId) {
|
||||
if (userId == null) {
|
||||
return null;
|
||||
}
|
||||
CommonResult<UserSignatureRespDTO> result = userSignatureApi.getByUserId(userId);
|
||||
return result == null ? null : result.getCheckedData();
|
||||
}
|
||||
|
||||
private <T> List<T> defaultList(List<T> list) {
|
||||
return list == null ? Collections.emptyList() : list;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.Month
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRefreshDraftReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSaveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSignOffConfirmReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSignOffDetailRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusLogRespVO;
|
||||
|
||||
@@ -32,12 +34,18 @@ public interface MonthlyReportService {
|
||||
|
||||
PageResult<MonthlyReportRespVO> getMonthlyApprovalPage(MonthlyReportPageReqVO reqVO);
|
||||
|
||||
PageResult<MonthlyReportRespVO> getMonthlySignOffPage(MonthlyReportPageReqVO reqVO);
|
||||
|
||||
MonthlyReportSignOffDetailRespVO getMonthlySignOffDetail(Long id);
|
||||
|
||||
void submitMonthlyReport(Long id);
|
||||
|
||||
void approveMonthlyReport(Long id, MonthlyReportApproveReqVO reqVO);
|
||||
|
||||
void rejectMonthlyReport(Long id, WorkReportStatusActionReqVO reqVO);
|
||||
|
||||
void confirmMonthlySignOff(Long id, MonthlyReportSignOffConfirmReqVO reqVO);
|
||||
|
||||
void deleteMonthlyReport(Long id);
|
||||
|
||||
List<WorkReportStatusLogRespVO> getMonthlyStatusLogs(Long id);
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.Month
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRefreshDraftReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSaveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSignOffConfirmReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportSignOffDetailRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportStatusLogRespVO;
|
||||
import com.njcn.rdms.module.project.service.workreport.common.WorkReportCommonService;
|
||||
@@ -71,6 +73,16 @@ public class MonthlyReportServiceImpl implements MonthlyReportService {
|
||||
return workReportCommonService.getMonthlyApprovalPage(reqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<MonthlyReportRespVO> getMonthlySignOffPage(MonthlyReportPageReqVO reqVO) {
|
||||
return workReportCommonService.getMonthlySignOffPage(reqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MonthlyReportSignOffDetailRespVO getMonthlySignOffDetail(Long id) {
|
||||
return workReportCommonService.getMonthlySignOffDetail(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submitMonthlyReport(Long id) {
|
||||
workReportCommonService.submitMonthlyReport(id);
|
||||
@@ -86,6 +98,11 @@ public class MonthlyReportServiceImpl implements MonthlyReportService {
|
||||
workReportCommonService.rejectMonthlyReport(id, reqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void confirmMonthlySignOff(Long id, MonthlyReportSignOffConfirmReqVO reqVO) {
|
||||
workReportCommonService.confirmMonthlySignOff(id, reqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMonthlyReport(Long id) {
|
||||
workReportCommonService.deleteMonthlyReport(id);
|
||||
|
||||
@@ -9,8 +9,13 @@ import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportMa
|
||||
import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportMapper;
|
||||
import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
|
||||
import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants;
|
||||
import com.njcn.rdms.module.system.api.notify.NotifyMessageSendApi;
|
||||
import com.njcn.rdms.module.system.api.notify.dto.NotifyUnreadMessageListReqDTO;
|
||||
import com.njcn.rdms.module.system.api.notify.dto.NotifyUnreadMessageRespDTO;
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -19,11 +24,16 @@ import org.springframework.util.StringUtils;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.IsoFields;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class WorkReportPendingSubmitRemindService {
|
||||
|
||||
private static final List<String> REMIND_STATUS_CODES = List.of(
|
||||
@@ -38,6 +48,8 @@ public class WorkReportPendingSubmitRemindService {
|
||||
private ProjectReportMapper projectReportMapper;
|
||||
@Resource
|
||||
private ApplicationEventPublisher applicationEventPublisher;
|
||||
@Resource
|
||||
private NotifyMessageSendApi notifyMessageSendApi;
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void remindPendingSubmitReports() {
|
||||
@@ -49,14 +61,21 @@ public class WorkReportPendingSubmitRemindService {
|
||||
|
||||
private void remindWeekly(LocalDateTime cutoffTime) {
|
||||
List<WeeklyReportDO> reports = weeklyReportMapper.selectListByPendingSubmitReminder(cutoffTime, REMIND_STATUS_CODES);
|
||||
Set<String> unreadKeys = loadUnreadReminderKeys(
|
||||
reports.stream().map(WeeklyReportDO::getReporterId).toList(),
|
||||
List.of(NotifyTemplateCodeConstants.WORK_REPORT_TEAM_REMIND));
|
||||
for (WeeklyReportDO report : reports) {
|
||||
if (report == null || report.getReporterId() == null) {
|
||||
continue;
|
||||
}
|
||||
if (hasUnreadReminder(report.getReporterId(), WorkReportConstants.REPORT_TYPE_WEEKLY, report.getId(), unreadKeys)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("reportTypeName", "周报");
|
||||
params.put("periodKey", formatWeeklyPeriod(report.getPeriodKey(), report.getPeriodStartDate(), report.getPeriodEndDate()));
|
||||
params.put("managerName", defaultText(report.getSupervisorName(), "系统通知"));
|
||||
appendRemindIdentity(params, WorkReportConstants.REPORT_TYPE_WEEKLY, report.getId());
|
||||
applicationEventPublisher.publishEvent(NotifySendEvent.of(
|
||||
List.of(report.getReporterId()),
|
||||
NotifyTemplateCodeConstants.WORK_REPORT_TEAM_REMIND,
|
||||
@@ -67,14 +86,21 @@ public class WorkReportPendingSubmitRemindService {
|
||||
|
||||
private void remindMonthly(LocalDateTime cutoffTime) {
|
||||
List<MonthlyReportDO> reports = monthlyReportMapper.selectListByPendingSubmitReminder(cutoffTime, REMIND_STATUS_CODES);
|
||||
Set<String> unreadKeys = loadUnreadReminderKeys(
|
||||
reports.stream().map(MonthlyReportDO::getReporterId).toList(),
|
||||
List.of(NotifyTemplateCodeConstants.WORK_REPORT_TEAM_REMIND));
|
||||
for (MonthlyReportDO report : reports) {
|
||||
if (report == null || report.getReporterId() == null) {
|
||||
continue;
|
||||
}
|
||||
if (hasUnreadReminder(report.getReporterId(), WorkReportConstants.REPORT_TYPE_MONTHLY, report.getId(), unreadKeys)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("reportTypeName", "月报");
|
||||
params.put("periodKey", formatMonthlyPeriod(report.getPeriodKey(), report.getPeriodStartDate()));
|
||||
params.put("managerName", defaultText(report.getSupervisorName(), "系统通知"));
|
||||
appendRemindIdentity(params, WorkReportConstants.REPORT_TYPE_MONTHLY, report.getId());
|
||||
applicationEventPublisher.publishEvent(NotifySendEvent.of(
|
||||
List.of(report.getReporterId()),
|
||||
NotifyTemplateCodeConstants.WORK_REPORT_TEAM_REMIND,
|
||||
@@ -85,14 +111,21 @@ public class WorkReportPendingSubmitRemindService {
|
||||
|
||||
private void remindProject(LocalDateTime cutoffTime) {
|
||||
List<ProjectReportDO> reports = projectReportMapper.selectListByPendingSubmitReminder(cutoffTime, REMIND_STATUS_CODES);
|
||||
Set<String> unreadKeys = loadUnreadReminderKeys(
|
||||
reports.stream().map(ProjectReportDO::getProjectOwnerId).toList(),
|
||||
List.of(NotifyTemplateCodeConstants.WORK_REPORT_PROJECT_HALF_MONTH_TEAM_REMIND));
|
||||
for (ProjectReportDO report : reports) {
|
||||
if (report == null || report.getProjectOwnerId() == null) {
|
||||
continue;
|
||||
}
|
||||
if (hasUnreadReminder(report.getProjectOwnerId(), WorkReportConstants.REPORT_TYPE_PROJECT, report.getId(), unreadKeys)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("projectName", defaultText(report.getProjectName(), ""));
|
||||
params.put("periodLabel", defaultText(report.getPeriodLabel(), report.getPeriodKey()));
|
||||
params.put("managerName", defaultText(report.getSupervisorName(), "系统通知"));
|
||||
appendRemindIdentity(params, WorkReportConstants.REPORT_TYPE_PROJECT, report.getId());
|
||||
applicationEventPublisher.publishEvent(NotifySendEvent.of(
|
||||
List.of(report.getProjectOwnerId()),
|
||||
NotifyTemplateCodeConstants.WORK_REPORT_PROJECT_HALF_MONTH_TEAM_REMIND,
|
||||
@@ -119,6 +152,58 @@ public class WorkReportPendingSubmitRemindService {
|
||||
return startDate.getYear() + "年" + startDate.getMonthValue() + "月";
|
||||
}
|
||||
|
||||
private Set<String> loadUnreadReminderKeys(Collection<Long> userIds, List<String> templateCodes) {
|
||||
if (userIds == null || userIds.isEmpty() || templateCodes == null || templateCodes.isEmpty()) {
|
||||
return Set.of();
|
||||
}
|
||||
List<Long> filteredUserIds = userIds.stream().filter(java.util.Objects::nonNull).distinct().toList();
|
||||
if (filteredUserIds.isEmpty()) {
|
||||
return Set.of();
|
||||
}
|
||||
try {
|
||||
CommonResult<List<NotifyUnreadMessageRespDTO>> result = notifyMessageSendApi.getUnreadNotifyMessageListByTemplateCodes(
|
||||
new NotifyUnreadMessageListReqDTO()
|
||||
.setUserIds(new ArrayList<>(filteredUserIds))
|
||||
.setTemplateCodes(templateCodes));
|
||||
List<NotifyUnreadMessageRespDTO> messages = result == null ? null : result.getCheckedData();
|
||||
if (messages == null || messages.isEmpty()) {
|
||||
return Set.of();
|
||||
}
|
||||
Set<String> keys = new HashSet<>();
|
||||
for (NotifyUnreadMessageRespDTO message : messages) {
|
||||
if (message == null || message.getUserId() == null || message.getTemplateParams() == null) {
|
||||
continue;
|
||||
}
|
||||
Object reportType = message.getTemplateParams().get(WorkReportConstants.PARAM_REMIND_REPORT_TYPE);
|
||||
Object reportId = message.getTemplateParams().get(WorkReportConstants.PARAM_REMIND_REPORT_ID);
|
||||
if (reportType == null || reportId == null) {
|
||||
continue;
|
||||
}
|
||||
keys.add(buildUnreadReminderKey(message.getUserId(), String.valueOf(reportType), String.valueOf(reportId)));
|
||||
}
|
||||
return keys;
|
||||
} catch (Exception ex) {
|
||||
log.warn("[loadUnreadReminderKeys][未读消息查询失败,本轮按无未读处理]", ex);
|
||||
return Set.of();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasUnreadReminder(Long userId, String reportType, Long reportId, Set<String> unreadKeys) {
|
||||
if (userId == null || reportId == null || unreadKeys == null || unreadKeys.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return unreadKeys.contains(buildUnreadReminderKey(userId, reportType, String.valueOf(reportId)));
|
||||
}
|
||||
|
||||
private String buildUnreadReminderKey(Long userId, String reportType, String reportId) {
|
||||
return userId + "#" + reportType + "#" + reportId;
|
||||
}
|
||||
|
||||
private void appendRemindIdentity(Map<String, Object> params, String reportType, Long reportId) {
|
||||
params.put(WorkReportConstants.PARAM_REMIND_REPORT_TYPE, reportType);
|
||||
params.put(WorkReportConstants.PARAM_REMIND_REPORT_ID, reportId == null ? null : String.valueOf(reportId));
|
||||
}
|
||||
|
||||
private String defaultText(String text, String fallback) {
|
||||
return StringUtils.hasText(text) ? text.trim() : fallback;
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ rdms:
|
||||
enabled: true
|
||||
weekly:
|
||||
enabled: true
|
||||
cron: "0 0 12 ? * FRI"
|
||||
cron: "0 0 12 ? * SUN"
|
||||
monthly:
|
||||
enabled: true
|
||||
cron: "0 0 12 1-31 * ?"
|
||||
@@ -132,10 +132,5 @@ rdms:
|
||||
cron: "0 0 12 1-31 * ?"
|
||||
scope:
|
||||
dept-ids: [100]
|
||||
performance:
|
||||
score-cell-mapping:
|
||||
actual-score-total: J10
|
||||
base-score-total: K10
|
||||
extra-score-total: L10
|
||||
|
||||
debug: false
|
||||
|
||||
@@ -7,6 +7,8 @@ import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -42,6 +44,30 @@ class ProjectObjectAuthorizationServiceTest extends BaseMockitoUnitTest {
|
||||
verify(projectObjectPermissionService).checkPermission(projectId, "project:task:status", false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAnyOwnerOrProjectPermission_whenCurrentUserMatchesAnyOwner_shouldSkipProjectPermissionCheck() {
|
||||
Long projectId = 1003L;
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(3002L)) {
|
||||
authorizationService.checkAnyOwnerOrProjectPermission(projectId,
|
||||
List.of(3001L, 3002L, 3003L), "project:task:update");
|
||||
}
|
||||
|
||||
verify(projectObjectPermissionService, never()).checkPermission(projectId, "project:task:update", false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAnyOwnerOrProjectPermission_whenCurrentUserMatchesNoOwner_shouldCheckProjectPermission() {
|
||||
Long projectId = 1004L;
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(3999L)) {
|
||||
authorizationService.checkAnyOwnerOrProjectPermission(projectId,
|
||||
List.of(3001L, 3002L, 3003L), "project:task:delete");
|
||||
}
|
||||
|
||||
verify(projectObjectPermissionService).checkPermission(projectId, "project:task:delete", false);
|
||||
}
|
||||
|
||||
private MockedStatic<SecurityFrameworkUtils> mockLoginUser(Long loginUserId) {
|
||||
MockedStatic<SecurityFrameworkUtils> mockedStatic = mockStatic(SecurityFrameworkUtils.class);
|
||||
mockedStatic.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(loginUserId);
|
||||
|
||||
@@ -1,726 +0,0 @@
|
||||
package com.njcn.rdms.module.project.service.product;
|
||||
|
||||
import com.njcn.rdms.framework.common.exception.ServiceException;
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.*;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementStatusLogDO;
|
||||
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.product.ProductRequirementMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementModuleMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementStatusLogMapper;
|
||||
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 org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 产品需求 Service 单元测试
|
||||
*/
|
||||
class ProductRequirementServiceImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private ProductRequirementServiceImpl requirementService;
|
||||
@Mock
|
||||
private ProductRequirementMapper requirementMapper;
|
||||
@Mock
|
||||
private ProductRequirementModuleMapper moduleMapper;
|
||||
@Mock
|
||||
private ProductRequirementStatusLogMapper statusLogMapper;
|
||||
@Mock
|
||||
private BizAuditLogMapper bizAuditLogMapper;
|
||||
@Mock
|
||||
private ObjectStatusTransitionMapper statusTransitionMapper;
|
||||
@Mock
|
||||
private ObjectStatusModelMapper statusModelMapper;
|
||||
@Mock
|
||||
private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;
|
||||
@Mock
|
||||
private com.njcn.rdms.module.project.dal.mysql.product.ProductMapper productMapper;
|
||||
@Mock
|
||||
private org.springframework.context.ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
// ========== 创建需求通知处理人测试(Task 9) ==========
|
||||
|
||||
/**
|
||||
* Task 9:创建产品需求后通知处理人(currentHandlerUserId),操作人由监听器统一排除。
|
||||
*/
|
||||
@Test
|
||||
void createRequirement_notifiesHandler() {
|
||||
try (MockedStatic<SecurityFrameworkUtils> mocked = mockStatic(SecurityFrameworkUtils.class)) {
|
||||
mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(9L);
|
||||
ProductRequirementDO req = new ProductRequirementDO();
|
||||
req.setId(1L);
|
||||
req.setProductId(20L);
|
||||
req.setTitle("电池告警");
|
||||
req.setCurrentHandlerUserId(5L);
|
||||
com.njcn.rdms.module.project.dal.dataobject.product.ProductDO product =
|
||||
new com.njcn.rdms.module.project.dal.dataobject.product.ProductDO();
|
||||
product.setName("智能终端");
|
||||
when(productMapper.selectById(20L)).thenReturn(product);
|
||||
|
||||
ArgumentCaptor<com.njcn.rdms.module.project.framework.notify.NotifySendEvent> captor =
|
||||
ArgumentCaptor.forClass(com.njcn.rdms.module.project.framework.notify.NotifySendEvent.class);
|
||||
requirementService.publishRequirementAssignedNotify(req);
|
||||
verify(applicationEventPublisher).publishEvent(captor.capture());
|
||||
com.njcn.rdms.module.project.framework.notify.NotifySendEvent ev = captor.getValue();
|
||||
assertEquals("product_requirement_assigned", ev.getTemplateCode());
|
||||
assertEquals(9L, ev.getOperatorUserId());
|
||||
assertTrue(ev.getUserIds().contains(5L));
|
||||
assertEquals("电池告警", ev.getParams().get("requirementTitle"));
|
||||
assertEquals("智能终端", ev.getParams().get("productName"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task 9:处理人为空时不发通知。
|
||||
*/
|
||||
@Test
|
||||
void createRequirement_whenHandlerNull_shouldNotPublish() {
|
||||
ProductRequirementDO req = new ProductRequirementDO();
|
||||
req.setId(1L);
|
||||
req.setProductId(20L);
|
||||
req.setTitle("电池告警");
|
||||
req.setCurrentHandlerUserId(null);
|
||||
|
||||
requirementService.publishRequirementAssignedNotify(req);
|
||||
verify(applicationEventPublisher, never()).publishEvent(any());
|
||||
}
|
||||
|
||||
// ========== 创建需求测试 ==========
|
||||
|
||||
@Test
|
||||
void createRequirement_withoutReview_shouldCreateWithPendingDispatchStatus() {
|
||||
Long loginUserId = 1001L;
|
||||
Long defaultModuleId = 50L;
|
||||
ProductRequirementSaveReqVO reqVO = new ProductRequirementSaveReqVO();
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setTitle("测试需求");
|
||||
reqVO.setDescription("测试描述");
|
||||
reqVO.setCategory("function");
|
||||
reqVO.setReviewRequired(0); // 不需要评审
|
||||
reqVO.setPriority(1);
|
||||
reqVO.setProposerId(2001L);
|
||||
reqVO.setProposerNickname("proposer-a");
|
||||
reqVO.setCurrentHandlerUserId(2002L);
|
||||
reqVO.setCurrentHandlerUserNickname("handler-a");
|
||||
|
||||
// 模拟"全部需求"模块存在(parentId = 0L 的根模块)
|
||||
ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, 100L);
|
||||
when(moduleMapper.selectByProductIdAndParentId(100L, 0L)).thenReturn(defaultModule);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.createRequirement(reqVO);
|
||||
}
|
||||
|
||||
ArgumentCaptor<ProductRequirementDO> captor = ArgumentCaptor.forClass(ProductRequirementDO.class);
|
||||
verify(requirementMapper, times(1)).insert(captor.capture());
|
||||
ProductRequirementDO created = captor.getValue();
|
||||
assertEquals("pending_dispatch", created.getStatusCode()); // 不需要评审时初始状态为待指派
|
||||
assertEquals("manual", created.getSourceType());
|
||||
assertEquals(0L, created.getParentId());
|
||||
assertEquals("测试需求", created.getTitle());
|
||||
assertEquals(defaultModuleId, created.getModuleId()); // 未选择模块时自动归属到"全部需求"模块
|
||||
assertEquals(100L, created.getProductId());
|
||||
assertEquals("handler-a", created.getCurrentHandlerUserNickname());
|
||||
|
||||
ArgumentCaptor<BizAuditLogDO> auditCaptor = ArgumentCaptor.forClass(BizAuditLogDO.class);
|
||||
verify(bizAuditLogMapper, times(1)).insert(auditCaptor.capture());
|
||||
assertNotNull(auditCaptor.getValue().getFieldChanges());
|
||||
assertTrue(auditCaptor.getValue().getFieldChanges().contains("currentHandlerUserNickname"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createRequirement_withReview_shouldCreateWithPendingReviewStatus() {
|
||||
Long loginUserId = 1001L;
|
||||
Long defaultModuleId = 50L;
|
||||
ProductRequirementSaveReqVO reqVO = new ProductRequirementSaveReqVO();
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setTitle("需评审的需求");
|
||||
reqVO.setCategory("function");
|
||||
reqVO.setReviewRequired(1); // 需要评审
|
||||
reqVO.setPriority(2);
|
||||
reqVO.setProposerId(2001L);
|
||||
|
||||
// 模拟"全部需求"模块存在(parentId = 0L 的根模块)
|
||||
ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, 100L);
|
||||
when(moduleMapper.selectByProductIdAndParentId(100L, 0L)).thenReturn(defaultModule);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.createRequirement(reqVO);
|
||||
}
|
||||
|
||||
ArgumentCaptor<ProductRequirementDO> captor = ArgumentCaptor.forClass(ProductRequirementDO.class);
|
||||
verify(requirementMapper, times(1)).insert(captor.capture());
|
||||
ProductRequirementDO created = captor.getValue();
|
||||
assertEquals("pending_review", created.getStatusCode()); // 需要评审时初始状态为待评审
|
||||
assertEquals(defaultModuleId, created.getModuleId()); // 未选择模块时自动归属到"全部需求"模块
|
||||
}
|
||||
|
||||
// ========== 更新需求测试 ==========
|
||||
|
||||
@Test
|
||||
void updateRequirement_whenTerminalStatus_shouldThrowException() {
|
||||
Long requirementId = 1001L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "已关闭需求",
|
||||
"closed", 0L, 0);
|
||||
ProductRequirementUpdateReqVO reqVO = new ProductRequirementUpdateReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setTitle("修改标题");
|
||||
reqVO.setCategory("function");
|
||||
reqVO.setReviewRequired(0);
|
||||
reqVO.setPriority(1);
|
||||
reqVO.setProposerId(2001L);
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.updateRequirement(reqVO));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_EDIT.getCode(), ex.getCode());
|
||||
verify(requirementMapper, never()).updateById(any(ProductRequirementDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateRequirement_whenNormalStatus_shouldUpdateSuccessfully() {
|
||||
Long requirementId = 1002L;
|
||||
Long loginUserId = 1001L;
|
||||
Long defaultModuleId = 50L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待指派需求",
|
||||
"pending_dispatch", 0L, 0);
|
||||
ProductRequirementUpdateReqVO reqVO = new ProductRequirementUpdateReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setTitle("修改后的标题");
|
||||
reqVO.setCategory("security");
|
||||
reqVO.setReviewRequired(0);
|
||||
reqVO.setPriority(2);
|
||||
reqVO.setProposerId(2001L);
|
||||
|
||||
// 模拟"全部需求"模块存在(未选择模块时自动归属,parentId = 0L 的根模块)
|
||||
ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, 100L);
|
||||
when(moduleMapper.selectByProductIdAndParentId(100L, 0L)).thenReturn(defaultModule);
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.updateRequirement(reqVO);
|
||||
}
|
||||
|
||||
ArgumentCaptor<ProductRequirementDO> captor = ArgumentCaptor.forClass(ProductRequirementDO.class);
|
||||
verify(requirementMapper, times(1)).updateById(captor.capture());
|
||||
assertEquals(defaultModuleId, captor.getValue().getModuleId()); // 未选择模块时自动归属到"全部需求"模块
|
||||
verify(bizAuditLogMapper, times(1)).insert(any(BizAuditLogDO.class));
|
||||
}
|
||||
|
||||
// ========== 状态变更测试 ==========
|
||||
|
||||
@Test
|
||||
void changeRequirementStatus_whenActionAllowed_shouldUpdateStatusAndWriteLogs() {
|
||||
Long requirementId = 1003L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待指派需求",
|
||||
"pending_dispatch", 0L, 0);
|
||||
ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
reqVO.setActionCode("dispatch");
|
||||
reqVO.setReason(null);
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
when(statusTransitionMapper.selectByObjectTypeAndFromStatusAndAction(
|
||||
"product_requirement", "pending_dispatch", "dispatch"))
|
||||
.thenReturn(createTransition("dispatch", "implementing", false));
|
||||
when(requirementMapper.updateStatusByIdAndStatus(requirementId, "pending_dispatch", "implementing", null))
|
||||
.thenReturn(1);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.changeRequirementStatus(reqVO);
|
||||
}
|
||||
|
||||
verify(requirementMapper, times(1))
|
||||
.updateStatusByIdAndStatus(requirementId, "pending_dispatch", "implementing", null);
|
||||
verify(statusLogMapper, times(1)).insert(any(ProductRequirementStatusLogDO.class));
|
||||
verify(bizAuditLogMapper, times(1)).insert(any(BizAuditLogDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeRequirementStatus_whenActionNotAllowed_shouldThrowException() {
|
||||
Long requirementId = 1004L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "已关闭需求",
|
||||
"closed", 0L, 0);
|
||||
ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
reqVO.setActionCode("dispatch");
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
when(statusTransitionMapper.selectByObjectTypeAndFromStatusAndAction(
|
||||
"product_requirement", "closed", "dispatch"))
|
||||
.thenReturn(null);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.changeRequirementStatus(reqVO));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_NOT_ALLOWED.getCode(), ex.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeRequirementStatus_whenReasonRequiredButMissing_shouldThrowException() {
|
||||
Long requirementId = 1005L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待指派需求",
|
||||
"pending_dispatch", 0L, 0);
|
||||
ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
reqVO.setActionCode("cancel");
|
||||
reqVO.setReason(" "); // 空原因
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
when(statusTransitionMapper.selectByObjectTypeAndFromStatusAndAction(
|
||||
"product_requirement", "pending_dispatch", "cancel"))
|
||||
.thenReturn(createTransition("cancel", "cancelled", true));
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.changeRequirementStatus(reqVO));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_REASON_REQUIRED.getCode(), ex.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeRequirementStatus_whenConcurrentModified_shouldThrowException() {
|
||||
Long requirementId = 1006L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待指派需求",
|
||||
"pending_dispatch", 0L, 0);
|
||||
ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
reqVO.setActionCode("dispatch");
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
when(statusTransitionMapper.selectByObjectTypeAndFromStatusAndAction(
|
||||
"product_requirement", "pending_dispatch", "dispatch"))
|
||||
.thenReturn(createTransition("dispatch", "implementing", false));
|
||||
when(requirementMapper.updateStatusByIdAndStatus(requirementId, "pending_dispatch", "implementing", null))
|
||||
.thenReturn(0); // 并发修改,更新失败
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.changeRequirementStatus(reqVO));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED.getCode(), ex.getCode());
|
||||
}
|
||||
verify(statusLogMapper, never()).insert(any(ProductRequirementStatusLogDO.class));
|
||||
verify(bizAuditLogMapper, never()).insert(any(BizAuditLogDO.class));
|
||||
}
|
||||
|
||||
// ========== 删除需求测试 ==========
|
||||
|
||||
@Test
|
||||
void deleteRequirement_shouldDeleteByConditionAndWriteLogs() {
|
||||
Long requirementId = 1007L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待指派需求",
|
||||
"pending_dispatch", 0L, 0);
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
when(requirementMapper.deleteByIdAndStatus(requirementId, "pending_dispatch")).thenReturn(1);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.deleteRequirement(requirementId, 100L);
|
||||
}
|
||||
|
||||
verify(requirementMapper, times(1)).deleteByIdAndStatus(requirementId, "pending_dispatch");
|
||||
verify(bizAuditLogMapper, times(1)).insert(any(BizAuditLogDO.class));
|
||||
}
|
||||
|
||||
// ========== 拆分需求测试 ==========
|
||||
|
||||
@Test
|
||||
void splitRequirement_whenParentPendingDispatch_shouldChangeParentToImplementing() {
|
||||
Long parentId = 1008L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementDO parent = createRequirement(parentId, 100L, "大需求",
|
||||
"pending_dispatch", 0L, 0);
|
||||
parent.setModuleId(50L);
|
||||
parent.setProposerId(2001L);
|
||||
ProductRequirementSplitReqVO reqVO = new ProductRequirementSplitReqVO();
|
||||
reqVO.setParentId(parentId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setTitle("子需求");
|
||||
reqVO.setCategory("function");
|
||||
reqVO.setPriority(1);
|
||||
|
||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
||||
when(requirementMapper.updateStatusByIdAndStatus(parentId, "pending_dispatch", "implementing", null))
|
||||
.thenReturn(1);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.splitRequirement(reqVO);
|
||||
}
|
||||
|
||||
// 验证子需求被创建
|
||||
ArgumentCaptor<ProductRequirementDO> childCaptor = ArgumentCaptor.forClass(ProductRequirementDO.class);
|
||||
verify(requirementMapper, times(1)).insert(childCaptor.capture());
|
||||
ProductRequirementDO child = childCaptor.getValue();
|
||||
assertEquals(parentId, child.getParentId());
|
||||
assertEquals("pending_dispatch", child.getStatusCode());
|
||||
assertEquals(parent.getModuleId(), child.getModuleId());
|
||||
assertEquals(reqVO.getProposerId(), child.getProposerId());
|
||||
|
||||
// 验证父需求状态变为实施中
|
||||
verify(requirementMapper, times(1))
|
||||
.updateStatusByIdAndStatus(parentId, "pending_dispatch", "implementing", null);
|
||||
verify(statusLogMapper, times(1)).insert(any(ProductRequirementStatusLogDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void splitRequirement_whenParentImplementing_shouldNotChangeParentStatus() {
|
||||
Long parentId = 1009L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementDO parent = createRequirement(parentId, 100L, "实施中的大需求",
|
||||
"implementing", 0L, 0);
|
||||
parent.setModuleId(50L);
|
||||
parent.setProposerId(2001L);
|
||||
ProductRequirementSplitReqVO reqVO = new ProductRequirementSplitReqVO();
|
||||
reqVO.setParentId(parentId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setTitle("子需求2");
|
||||
reqVO.setCategory("function");
|
||||
reqVO.setPriority(1);
|
||||
|
||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.splitRequirement(reqVO);
|
||||
}
|
||||
|
||||
// 父需求已经是实施中,不需要再更新状态
|
||||
verify(requirementMapper, never())
|
||||
.updateStatusByIdAndStatus(parentId, "implementing", "implementing", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void splitRequirement_whenParentNotAllowSplit_shouldThrowException() {
|
||||
Long parentId = 1010L;
|
||||
ProductRequirementDO parent = createRequirement(parentId, 100L, "已验收的大需求",
|
||||
"accepted", 0L, 0);
|
||||
ProductRequirementSplitReqVO reqVO = new ProductRequirementSplitReqVO();
|
||||
reqVO.setParentId(parentId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setTitle("子需求");
|
||||
reqVO.setCategory("function");
|
||||
reqVO.setPriority(1);
|
||||
|
||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.splitRequirement(reqVO));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_PARENT_NOT_ALLOW_SPLIT.getCode(), ex.getCode());
|
||||
verify(requirementMapper, never()).insert(any(ProductRequirementDO.class));
|
||||
}
|
||||
|
||||
// ========== 关闭需求测试 ==========
|
||||
|
||||
@Test
|
||||
void closeRequirement_whenAccepted_shouldCloseSuccessfully() {
|
||||
Long requirementId = 1011L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "已验收需求",
|
||||
"accepted", 0L, 0);
|
||||
ProductRequirementCloseReqVO reqVO = new ProductRequirementCloseReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setReason("需求已完成");
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
when(requirementMapper.selectListByParentId(requirementId)).thenReturn(Collections.emptyList());
|
||||
when(requirementMapper.updateStatusByIdAndStatus(requirementId, "accepted", "closed", "需求已完成"))
|
||||
.thenReturn(1);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.closeRequirement(reqVO);
|
||||
}
|
||||
|
||||
verify(requirementMapper, times(1))
|
||||
.updateStatusByIdAndStatus(requirementId, "accepted", "closed", "需求已完成");
|
||||
verify(statusLogMapper, times(1)).insert(any(ProductRequirementStatusLogDO.class));
|
||||
verify(bizAuditLogMapper, times(1)).insert(any(BizAuditLogDO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void closeRequirement_whenNotAccepted_shouldThrowException() {
|
||||
Long requirementId = 1012L;
|
||||
ProductRequirementDO requirement = createRequirement(requirementId, 100L, "实施中需求",
|
||||
"implementing", 0L, 0);
|
||||
ProductRequirementCloseReqVO reqVO = new ProductRequirementCloseReqVO();
|
||||
reqVO.setId(requirementId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setReason("需求已完成");
|
||||
|
||||
when(requirementMapper.selectById(requirementId)).thenReturn(requirement);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.closeRequirement(reqVO));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_CLOSE.getCode(), ex.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void closeRequirement_withChildren_shouldCascadeCloseAcceptedChildren() {
|
||||
Long parentId = 1013L;
|
||||
Long childId = 1014L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementDO parent = createRequirement(parentId, 100L, "父需求",
|
||||
"accepted", 0L, 0);
|
||||
ProductRequirementDO child = createRequirement(childId, 100L, "子需求",
|
||||
"accepted", parentId, 0);
|
||||
|
||||
ProductRequirementCloseReqVO reqVO = new ProductRequirementCloseReqVO();
|
||||
reqVO.setId(parentId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setReason("全部验收完成");
|
||||
|
||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
||||
when(requirementMapper.selectListByParentId(parentId)).thenReturn(List.of(child));
|
||||
when(requirementMapper.updateStatusByIdAndStatus(childId, "accepted", "closed", "全部验收完成"))
|
||||
.thenReturn(1);
|
||||
when(requirementMapper.updateStatusByIdAndStatus(parentId, "accepted", "closed", "全部验收完成"))
|
||||
.thenReturn(1);
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.closeRequirement(reqVO);
|
||||
}
|
||||
|
||||
// 子需求被关闭
|
||||
verify(requirementMapper, times(1))
|
||||
.updateStatusByIdAndStatus(childId, "accepted", "closed", "全部验收完成");
|
||||
// 父需求被关闭
|
||||
verify(requirementMapper, times(1))
|
||||
.updateStatusByIdAndStatus(parentId, "accepted", "closed", "全部验收完成");
|
||||
}
|
||||
|
||||
@Test
|
||||
void closeRequirement_withChildNotAllowClose_shouldThrowException() {
|
||||
Long parentId = 1015L;
|
||||
ProductRequirementDO parent = createRequirement(parentId, 100L, "父需求",
|
||||
"accepted", 0L, 0);
|
||||
ProductRequirementDO child = createRequirement(1016L, 100L, "实施中子需求",
|
||||
"implementing", parentId, 0);
|
||||
|
||||
ProductRequirementCloseReqVO reqVO = new ProductRequirementCloseReqVO();
|
||||
reqVO.setId(parentId);
|
||||
reqVO.setProductId(100L);
|
||||
reqVO.setReason("全部验收完成");
|
||||
|
||||
when(requirementMapper.selectById(parentId)).thenReturn(parent);
|
||||
when(requirementMapper.selectListByParentId(parentId)).thenReturn(List.of(child));
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.closeRequirement(reqVO));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_CHILD_NOT_ALLOW_CLOSE.getCode(), ex.getCode());
|
||||
}
|
||||
|
||||
// ========== 模块管理测试 ==========
|
||||
|
||||
@Test
|
||||
void createRequirementModule_whenNameDuplicate_shouldThrowException() {
|
||||
Long productId = 100L;
|
||||
ProductRequirementModuleDO existModule = new ProductRequirementModuleDO();
|
||||
existModule.setId(50L);
|
||||
existModule.setProductId(productId);
|
||||
existModule.setModuleName("核心功能");
|
||||
|
||||
ProductRequirementModuleReqVO reqVO = new ProductRequirementModuleReqVO();
|
||||
reqVO.setProductId(productId);
|
||||
reqVO.setModuleName("核心功能");
|
||||
|
||||
when(moduleMapper.selectByProductIdAndModuleName(productId, "核心功能")).thenReturn(existModule);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.createRequirementModule(reqVO));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_MODULE_NAME_DUPLICATE.getCode(), ex.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteRequirementModule_whenHasNonTerminalRequirements_shouldThrowException() {
|
||||
Long moduleId = 50L;
|
||||
Long productId = 100L;
|
||||
ProductRequirementModuleDO module = new ProductRequirementModuleDO();
|
||||
module.setId(moduleId);
|
||||
module.setProductId(productId);
|
||||
module.setModuleName("核心功能");
|
||||
|
||||
ProductRequirementDO nonTerminalReq = createRequirement(2001L, productId, "实施中需求",
|
||||
"implementing", 0L, 0);
|
||||
nonTerminalReq.setModuleId(moduleId);
|
||||
|
||||
when(moduleMapper.selectById(moduleId)).thenReturn(module);
|
||||
when(requirementMapper.selectListByModuleId(moduleId)).thenReturn(List.of(nonTerminalReq));
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.deleteRequirementModule(moduleId, productId));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_MODULE_HAS_NON_TERMINAL_REQUIREMENTS.getCode(), ex.getCode());
|
||||
verify(moduleMapper, never()).deleteById(moduleId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteRequirementModule_whenAllTerminal_shouldDeleteSuccessfully() {
|
||||
Long moduleId = 50L;
|
||||
Long productId = 100L;
|
||||
Long loginUserId = 1001L;
|
||||
ProductRequirementModuleDO module = new ProductRequirementModuleDO();
|
||||
module.setId(moduleId);
|
||||
module.setProductId(productId);
|
||||
module.setModuleName("核心功能");
|
||||
|
||||
ProductRequirementDO closedReq = createRequirement(2001L, productId, "已关闭需求",
|
||||
"closed", 0L, 0);
|
||||
closedReq.setModuleId(moduleId);
|
||||
|
||||
when(moduleMapper.selectById(moduleId)).thenReturn(module);
|
||||
when(requirementMapper.selectListByModuleId(moduleId)).thenReturn(List.of(closedReq));
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
|
||||
requirementService.deleteRequirementModule(moduleId, productId);
|
||||
}
|
||||
|
||||
verify(requirementMapper, times(1)).deleteById(2001L);
|
||||
verify(moduleMapper, times(1)).deleteById(moduleId);
|
||||
}
|
||||
|
||||
// ========== resolveModuleId 测试 ==========
|
||||
|
||||
@Test
|
||||
void resolveModuleId_whenModuleIdIsNull_shouldReturnDefaultModuleId() {
|
||||
Long defaultModuleId = 50L;
|
||||
Long productId = 100L;
|
||||
ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, productId);
|
||||
|
||||
when(moduleMapper.selectByProductIdAndParentId(productId, 0L)).thenReturn(defaultModule);
|
||||
|
||||
Long result = requirementService.resolveModuleId(null, productId);
|
||||
|
||||
assertEquals(defaultModuleId, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveModuleId_whenModuleIdProvided_shouldReturnSameId() {
|
||||
Long moduleId = 60L;
|
||||
Long productId = 100L;
|
||||
|
||||
// 传入有效moduleId时直接返回,不查询数据库
|
||||
Long result = requirementService.resolveModuleId(moduleId, productId);
|
||||
|
||||
assertEquals(moduleId, result);
|
||||
verify(moduleMapper, never()).selectByProductIdAndParentId(any(), any());
|
||||
}
|
||||
|
||||
// ========== isAllRequirementsModule 测试 ==========
|
||||
|
||||
@Test
|
||||
void isAllRequirementsModule_whenRootModule_shouldReturnTrue() {
|
||||
Long moduleId = 50L;
|
||||
ProductRequirementModuleDO rootModule = createDefaultModule(moduleId, 100L);
|
||||
|
||||
when(moduleMapper.selectById(moduleId)).thenReturn(rootModule);
|
||||
|
||||
boolean result = requirementService.isAllRequirementsModule(moduleId);
|
||||
|
||||
assertEquals(true, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllRequirementsModule_whenChildModule_shouldReturnFalse() {
|
||||
Long moduleId = 60L;
|
||||
ProductRequirementModuleDO childModule = new ProductRequirementModuleDO();
|
||||
childModule.setId(moduleId);
|
||||
childModule.setProductId(100L);
|
||||
childModule.setParentId(50L); // parentId != 0
|
||||
childModule.setModuleName("子模块");
|
||||
|
||||
when(moduleMapper.selectById(moduleId)).thenReturn(childModule);
|
||||
|
||||
boolean result = requirementService.isAllRequirementsModule(moduleId);
|
||||
|
||||
assertEquals(false, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllRequirementsModule_whenModuleNotExists_shouldReturnFalse() {
|
||||
Long moduleId = 70L;
|
||||
|
||||
when(moduleMapper.selectById(moduleId)).thenReturn(null);
|
||||
|
||||
boolean result = requirementService.isAllRequirementsModule(moduleId);
|
||||
|
||||
assertEquals(false, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveModuleId_whenDefaultModuleNotExists_shouldThrowException() {
|
||||
Long productId = 100L;
|
||||
|
||||
when(moduleMapper.selectByProductIdAndParentId(productId, 0L)).thenReturn(null);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> requirementService.resolveModuleId(null, productId));
|
||||
assertEquals(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS.getCode(), ex.getCode());
|
||||
}
|
||||
|
||||
// ========== 辅助方法 ==========
|
||||
|
||||
private ProductRequirementDO createRequirement(Long id, Long productId, String title,
|
||||
String statusCode, Long parentId, Integer reviewRequired) {
|
||||
ProductRequirementDO requirement = new ProductRequirementDO();
|
||||
requirement.setId(id);
|
||||
requirement.setProductId(productId);
|
||||
requirement.setTitle(title);
|
||||
requirement.setStatusCode(statusCode);
|
||||
requirement.setParentId(parentId);
|
||||
requirement.setReviewRequired(reviewRequired);
|
||||
requirement.setCategory("function");
|
||||
requirement.setSourceType("manual");
|
||||
requirement.setPriority(1);
|
||||
requirement.setProposerId(2001L);
|
||||
requirement.setSort(0);
|
||||
return requirement;
|
||||
}
|
||||
|
||||
private ProductRequirementModuleDO createDefaultModule(Long id, Long productId) {
|
||||
ProductRequirementModuleDO module = new ProductRequirementModuleDO();
|
||||
module.setId(id);
|
||||
module.setProductId(productId);
|
||||
module.setParentId(0L);
|
||||
module.setModuleName("全部需求");
|
||||
module.setRemark("自动创建的模块");
|
||||
module.setSort(0);
|
||||
return module;
|
||||
}
|
||||
|
||||
private ObjectStatusTransitionDO createTransition(String actionCode, String toStatus, boolean needReason) {
|
||||
ObjectStatusTransitionDO transition = new ObjectStatusTransitionDO();
|
||||
transition.setActionCode(actionCode);
|
||||
transition.setToStatusCode(toStatus);
|
||||
transition.setNeedReason(needReason);
|
||||
return transition;
|
||||
}
|
||||
|
||||
private MockedStatic<SecurityFrameworkUtils> mockLoginUser(Long loginUserId, String nickname) {
|
||||
MockedStatic<SecurityFrameworkUtils> mockedStatic = mockStatic(SecurityFrameworkUtils.class);
|
||||
mockedStatic.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(loginUserId);
|
||||
mockedStatic.when(SecurityFrameworkUtils::getLoginUserNickname).thenReturn(nickname);
|
||||
return mockedStatic;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,10 +10,12 @@ import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execut
|
||||
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.ProjectExecutionRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionCreateReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionDeleteReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionUpdateReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
|
||||
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
|
||||
import com.njcn.rdms.module.project.constant.ProjectExecutionConstants;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionAssigneeDO;
|
||||
@@ -35,6 +37,7 @@ import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
|
||||
import com.njcn.rdms.module.project.framework.notify.NotifyJumpConstants;
|
||||
import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
|
||||
import com.njcn.rdms.module.project.framework.security.service.ProjectObjectAuthorizationService;
|
||||
import com.njcn.rdms.module.project.service.project.ProjectRequirementService;
|
||||
import com.njcn.rdms.module.system.api.dict.DictDataApi;
|
||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||
@@ -107,6 +110,8 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
||||
@Mock
|
||||
private ProjectRequirementMapper projectRequirementMapper;
|
||||
@Mock
|
||||
private ProjectObjectAuthorizationService projectObjectAuthorizationService;
|
||||
@Mock
|
||||
private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;
|
||||
@Mock
|
||||
private org.springframework.context.ApplicationEventPublisher applicationEventPublisher;
|
||||
@@ -119,6 +124,8 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
||||
void setupDefaultPriorityValidation() {
|
||||
lenient().when(dictDataApi.validateDictDataList(eq(ProjectDictTypeConstants.REQ_PRIORITY), any()))
|
||||
.thenReturn(success(true));
|
||||
lenient().when(userObjectRoleMapper.selectActiveListByObjectAndUserId(eq("project"), any(), eq(3001L)))
|
||||
.thenAnswer(invocation -> List.of(createProjectMember(invocation.getArgument(1), 3001L, 3101L)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -130,6 +137,7 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
||||
reqVO.setOwnerId(3001L);
|
||||
reqVO.setProjectRequirementId(null);
|
||||
reqVO.setAssigneeUserIds(List.of(3002L, 3003L));
|
||||
reqVO.setPriority("1");
|
||||
|
||||
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
||||
@@ -203,6 +211,7 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
||||
reqVO.setOwnerId(3001L);
|
||||
reqVO.setProjectRequirementId(9001L);
|
||||
reqVO.setAssigneeUserIds(List.of(3002L));
|
||||
reqVO.setPriority("1");
|
||||
|
||||
when(projectMapper.selectById(projectId)).thenReturn(createEditableProject(projectId));
|
||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
||||
@@ -269,8 +278,6 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
||||
.thenReturn(createProjectStatus("pending", true));
|
||||
when(projectExecutionMapper.selectByProjectIdAndName(projectId, "后端接口联调")).thenReturn(null);
|
||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, 3001L))
|
||||
.thenReturn(List.of(createProjectMember(projectId, 3001L, 3101L)));
|
||||
when(dictDataApi.validateDictDataList(ProjectDictTypeConstants.EXECUTION_TYPE, List.of("feature")))
|
||||
.thenReturn(success(true));
|
||||
|
||||
@@ -299,8 +306,6 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
||||
.thenReturn(createExecutionStatus("pending", true));
|
||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId)).thenReturn(execution);
|
||||
when(projectExecutionMapper.selectByProjectIdAndName(projectId, "接口联调-修订")).thenReturn(null);
|
||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, 3001L))
|
||||
.thenReturn(List.of(createProjectMember(projectId, 3001L, 3101L)));
|
||||
when(dictDataApi.validateDictDataList(ProjectDictTypeConstants.EXECUTION_TYPE, List.of("feature")))
|
||||
.thenReturn(success(true));
|
||||
|
||||
@@ -310,6 +315,36 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
||||
verify(projectExecutionMapper).updateById(execution);
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateExecution_shouldAuthorizeProjectManagerOrExecutionOwnerBeforeUpdating() {
|
||||
Long projectId = 2001L;
|
||||
Long executionId = 5001L;
|
||||
ProjectDO project = createEditableProject(projectId);
|
||||
project.setManagerUserId(3001L);
|
||||
ProjectExecutionDO execution = createExecution(projectId, executionId, 3002L);
|
||||
ProjectExecutionUpdateReqVO reqVO = new ProjectExecutionUpdateReqVO();
|
||||
reqVO.setId(executionId);
|
||||
reqVO.setExecutionName("执行权限联调");
|
||||
reqVO.setExecutionType("feature");
|
||||
reqVO.setPriority("1");
|
||||
reqVO.setProjectRequirementId(null);
|
||||
|
||||
when(projectMapper.selectById(projectId)).thenReturn(project);
|
||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
||||
.thenReturn(createProjectStatus("pending", true));
|
||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("execution", "pending"))
|
||||
.thenReturn(createExecutionStatus("pending", true));
|
||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId)).thenReturn(execution);
|
||||
when(projectExecutionMapper.selectByProjectIdAndName(projectId, "执行权限联调")).thenReturn(null);
|
||||
when(dictDataApi.validateDictDataList(ProjectDictTypeConstants.EXECUTION_TYPE, List.of("feature")))
|
||||
.thenReturn(success(true));
|
||||
|
||||
projectExecutionService.updateExecution(projectId, reqVO);
|
||||
|
||||
verify(projectObjectAuthorizationService).checkAnyOwnerOrProjectPermission(projectId,
|
||||
List.of(3001L, 3002L), ProjectExecutionConstants.PERMISSION_UPDATE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeOwner_shouldUpdateOwnerAndWriteOwnerTransferLogs() {
|
||||
Long projectId = 2001L;
|
||||
@@ -340,6 +375,51 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
||||
ObjectActivityConstants.EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_IN, "负责人调整");
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteExecution_shouldAuthorizeProjectManagerOrExecutionOwnerBeforeDeleting() {
|
||||
Long projectId = 2001L;
|
||||
Long executionId = 5001L;
|
||||
ProjectDO project = createEditableProject(projectId);
|
||||
project.setManagerUserId(3001L);
|
||||
ProjectExecutionDO execution = createExecution(projectId, executionId, 3002L);
|
||||
execution.setExecutionName("Execution auth");
|
||||
ProjectExecutionDeleteReqVO reqVO = new ProjectExecutionDeleteReqVO();
|
||||
reqVO.setExecutionName("Execution auth");
|
||||
reqVO.setConfirmText("DELETE");
|
||||
reqVO.setReason("录入错误");
|
||||
|
||||
when(projectMapper.selectById(projectId)).thenReturn(project);
|
||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId)).thenReturn(execution);
|
||||
when(projectTaskMapper.selectIdsByExecutionId(executionId)).thenReturn(Collections.emptyList());
|
||||
when(projectExecutionMapper.deleteById(executionId)).thenReturn(1);
|
||||
|
||||
projectExecutionService.deleteExecution(projectId, executionId, reqVO);
|
||||
|
||||
verify(projectObjectAuthorizationService).checkAnyOwnerOrProjectPermission(projectId,
|
||||
List.of(3001L, 3002L), ProjectExecutionConstants.PERMISSION_DELETE);
|
||||
verify(projectExecutionMapper).deleteById(executionId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void precheckDeleteExecution_shouldAuthorizeProjectManagerOrExecutionOwnerBeforeCountingTasks() {
|
||||
Long projectId = 2001L;
|
||||
Long executionId = 5001L;
|
||||
ProjectDO project = createEditableProject(projectId);
|
||||
project.setManagerUserId(3001L);
|
||||
ProjectExecutionDO execution = createExecution(projectId, executionId, 3002L);
|
||||
|
||||
when(projectMapper.selectById(projectId)).thenReturn(project);
|
||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId)).thenReturn(execution);
|
||||
when(projectTaskMapper.countByExecutionId(executionId)).thenReturn(3);
|
||||
|
||||
var result = projectExecutionService.precheckDeleteExecution(projectId, executionId);
|
||||
|
||||
verify(projectObjectAuthorizationService).checkAnyOwnerOrProjectPermission(projectId,
|
||||
List.of(3001L, 3002L), ProjectExecutionConstants.PERMISSION_DELETE);
|
||||
assertEquals(3, result.getTaskCount());
|
||||
assertEquals(true, result.getHasDependentData());
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeExecutionStatus_shouldUpdateByTransitionAndWriteStatusAndAuditLogs() {
|
||||
Long projectId = 2001L;
|
||||
@@ -771,8 +851,6 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
||||
when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
|
||||
.thenReturn(createProjectStatus("pending", true));
|
||||
when(projectExecutionMapper.selectByProjectIdAndName(projectId, "接口联调")).thenReturn(null);
|
||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, 3001L))
|
||||
.thenReturn(List.of(createProjectMember(projectId, 3001L, 3101L)));
|
||||
when(dictDataApi.validateDictDataList(eq(ProjectDictTypeConstants.EXECUTION_TYPE), any()))
|
||||
.thenReturn(success(true));
|
||||
|
||||
@@ -877,8 +955,6 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
||||
.thenReturn(createExecutionStatus("pending", true));
|
||||
when(projectExecutionMapper.selectByProjectIdAndId(projectId, executionId)).thenReturn(execution);
|
||||
when(projectExecutionMapper.selectByProjectIdAndName(projectId, "接口联调")).thenReturn(null);
|
||||
when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, 3001L))
|
||||
.thenReturn(List.of(createProjectMember(projectId, 3001L, 3101L)));
|
||||
when(dictDataApi.validateDictDataList(eq(ProjectDictTypeConstants.EXECUTION_TYPE), any()))
|
||||
.thenReturn(success(true));
|
||||
when(dictDataApi.validateDictDataList(eq(ProjectDictTypeConstants.REQ_PRIORITY), any()))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.njcn.rdms.module.system.api.file;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.module.system.api.file.dto.FileContentCreateReqDTO;
|
||||
import com.njcn.rdms.module.system.api.file.dto.FileRespDTO;
|
||||
import com.njcn.rdms.module.system.enums.ApiConstants;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -8,6 +9,8 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@FeignClient(name = ApiConstants.NAME)
|
||||
@@ -31,4 +34,8 @@ public interface FileApi {
|
||||
@Parameter(name = "id", description = "文件 ID", required = true)
|
||||
CommonResult<byte[]> getFileContent(@RequestParam("id") Long id);
|
||||
|
||||
@PostMapping(PREFIX + "/create-by-content")
|
||||
@Operation(summary = "通过文件字节创建文件")
|
||||
CommonResult<Long> createFileByContent(@RequestBody FileContentCreateReqDTO reqDTO);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.njcn.rdms.module.system.api.file.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "RPC 服务 - 文件字节创建 Request DTO")
|
||||
@Data
|
||||
public class FileContentCreateReqDTO {
|
||||
|
||||
@Schema(description = "文件内容", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private byte[] content;
|
||||
|
||||
@Schema(description = "文件名", example = "2026-06_张三.xlsx")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "目录", example = "rdms/performance")
|
||||
private String directory;
|
||||
|
||||
@Schema(description = "MIME 类型", example = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
private String type;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.njcn.rdms.module.system.api.user;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.module.system.api.user.dto.UserSignatureRespDTO;
|
||||
import com.njcn.rdms.module.system.enums.ApiConstants;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@FeignClient(name = ApiConstants.NAME)
|
||||
@Tag(name = "RPC 服务 - 用户电子签名")
|
||||
public interface UserSignatureApi {
|
||||
|
||||
String PREFIX = ApiConstants.PREFIX + "/user-signature";
|
||||
|
||||
@GetMapping(PREFIX + "/get")
|
||||
@Operation(summary = "根据用户 ID 查询电子签名")
|
||||
@Parameter(name = "userId", description = "用户 ID", required = true)
|
||||
CommonResult<UserSignatureRespDTO> getByUserId(@RequestParam("userId") Long userId);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.njcn.rdms.module.system.api.user.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "RPC 服务 - 用户电子签名 Response DTO")
|
||||
@Data
|
||||
public class UserSignatureRespDTO {
|
||||
|
||||
@Schema(description = "主键", example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户 ID", example = "1024")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "文件 ID", example = "2048")
|
||||
private Long fileId;
|
||||
|
||||
@Schema(description = "文件名", example = "zhangsan-sign.png")
|
||||
private String fileName;
|
||||
|
||||
@Schema(description = "状态", example = "0")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
@@ -106,6 +106,7 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode FILE_PATH_EXISTS = new ErrorCode(1_001_003_000, "文件路径已存在");
|
||||
ErrorCode FILE_NOT_EXISTS = new ErrorCode(1_001_003_001, "文件不存在");
|
||||
ErrorCode FILE_IS_EMPTY = new ErrorCode(1_001_003_002, "文件为空");
|
||||
ErrorCode FILE_DELETE_NO_PERMISSION = new ErrorCode(1_001_003_003, "无权删除该文件,仅可删除本人上传的文件");
|
||||
|
||||
// ========== OAuth2 客户端 1-002-020-000 =========
|
||||
ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1_002_020_000, "OAuth2 客户端不存在");
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.njcn.rdms.module.system.api.file;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||
import com.njcn.rdms.module.system.api.file.dto.FileContentCreateReqDTO;
|
||||
import com.njcn.rdms.module.system.api.file.dto.FileRespDTO;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.file.FileDO;
|
||||
import com.njcn.rdms.module.system.service.file.FileService;
|
||||
@@ -45,4 +46,10 @@ public class FileApiImpl implements FileApi {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<Long> createFileByContent(FileContentCreateReqDTO reqDTO) {
|
||||
FileDO file = fileService.createFile(reqDTO.getContent(), reqDTO.getName(), reqDTO.getDirectory(), reqDTO.getType());
|
||||
return success(file.getId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.njcn.rdms.module.system.api.user;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||
import com.njcn.rdms.module.system.api.user.dto.UserSignatureRespDTO;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.user.UserSignatureDO;
|
||||
import com.njcn.rdms.module.system.service.user.UserSignatureService;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@RestController
|
||||
@Validated
|
||||
@Hidden
|
||||
public class UserSignatureApiImpl implements UserSignatureApi {
|
||||
|
||||
@Resource
|
||||
private UserSignatureService userSignatureService;
|
||||
|
||||
@Override
|
||||
public CommonResult<UserSignatureRespDTO> getByUserId(Long userId) {
|
||||
UserSignatureDO signature = userSignatureService.getByUserId(userId);
|
||||
return success(BeanUtils.toBean(signature, UserSignatureRespDTO.class));
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,8 @@ public class FileController {
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除文件")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('system:file:delete')")
|
||||
// 权限下沉到 FileService:上传者本人可删自己上传的文件,其余需 system:file:delete 权限。
|
||||
// 故意不挂 @PreAuthorize,否则无该权限码的上传者会在进方法体前被 AOP 挡死(口径同项目域"属主放行+权限码兜底")。
|
||||
public CommonResult<Boolean> deleteFile(@RequestParam("id") Long id) throws Exception {
|
||||
fileService.deleteFile(id);
|
||||
return success(true);
|
||||
@@ -93,7 +94,7 @@ public class FileController {
|
||||
@DeleteMapping("/delete-list")
|
||||
@Operation(summary = "批量删除文件")
|
||||
@Parameter(name = "ids", description = "编号列表", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('system:file:delete')")
|
||||
// 权限下沉到 FileService:逐个按上传者归属校验,其余需 system:file:delete 权限(见 deleteFile 注释)。
|
||||
public CommonResult<Boolean> deleteFileList(@RequestParam("ids") List<Long> ids) throws Exception {
|
||||
fileService.deleteFileList(ids);
|
||||
return success(true);
|
||||
|
||||
@@ -136,7 +136,7 @@ public class UserManagementRelationController {
|
||||
* 获得用户管理链路信息
|
||||
*
|
||||
* 根据主键ID查询单条用户管理链路记录
|
||||
* 权限要求:system:user-management-relation:query
|
||||
* 权限要求:仅需登录,不做权限码校验
|
||||
*
|
||||
* @param id 关系记录主键ID
|
||||
* @return 用户管理链路详情
|
||||
@@ -144,7 +144,6 @@ public class UserManagementRelationController {
|
||||
@GetMapping(value = "/get")
|
||||
@Operation(summary = "获得用户管理链路信息")
|
||||
@Parameter(name = "id", description = "关系编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('system:user-management-relation:query')")
|
||||
public CommonResult<UserManagementRelationRespVO> getUserManagementRelation(@RequestParam("id") Long id) {
|
||||
UserManagementRelationDO relation = userManagementRelationService.getRelation(id);
|
||||
return success(BeanUtils.toBean(relation, UserManagementRelationRespVO.class));
|
||||
@@ -154,14 +153,13 @@ public class UserManagementRelationController {
|
||||
* 获取用户管理链路列表
|
||||
*
|
||||
* 根据查询条件查询用户管理链路记录列表
|
||||
* 权限要求:system:user-management-relation:query
|
||||
* 权限要求:仅需登录,不做权限码校验
|
||||
*
|
||||
* @param reqVO 查询条件VO
|
||||
* @return 用户管理链路列表
|
||||
*/
|
||||
@GetMapping("/query")
|
||||
@Operation(summary = "获取用户管理链路列表")
|
||||
@PreAuthorize("@ss.hasPermission('system:user-management-relation:query')")
|
||||
public CommonResult<List<UserManagementRelationTreeRespVO>> getUserManagementRelationQuery(@Validated UserManagementRelationQueryReqVO reqVO) {
|
||||
List<UserManagementRelationTreeRespVO> list = userManagementRelationService.getRelationQuery(reqVO);
|
||||
return success(list);
|
||||
@@ -176,7 +174,6 @@ public class UserManagementRelationController {
|
||||
@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) {
|
||||
@@ -193,7 +190,6 @@ public class UserManagementRelationController {
|
||||
*/
|
||||
@GetMapping("/candidate-users")
|
||||
@Operation(summary = "获取未绑定直属上级的候选下级用户列表")
|
||||
@PreAuthorize("@ss.hasPermission('system:user-management-relation:query')")
|
||||
public CommonResult<List<UserSimpleRespVO>> getCandidateSubordinateUsers() {
|
||||
List<AdminUserDO> users = userManagementRelationService.getCandidateSubordinateUsers();
|
||||
Map<Long, DeptDO> deptMap = deptService.getDeptMap(convertList(users, AdminUserDO::getDeptId));
|
||||
@@ -208,7 +204,6 @@ public class UserManagementRelationController {
|
||||
@GetMapping("/direct-subordinates")
|
||||
@Operation(summary = "获取某用户当前生效的直属下级列表")
|
||||
@Parameter(name = "userId", description = "用户ID", required = true, example = "1024")
|
||||
//@PreAuthorize("@ss.hasPermission('system:user-management-relation:query')")
|
||||
public CommonResult<List<UserSimpleRespVO>> getDirectSubordinates(@RequestParam("userId") Long userId) {
|
||||
List<UserManagementRelationDO> relations = userManagementRelationService.getRelationListByManagerUserId(userId);
|
||||
if (relations.isEmpty()) {
|
||||
@@ -250,13 +245,12 @@ public class UserManagementRelationController {
|
||||
* - 中间节点:有上级也有下级
|
||||
* - 叶子节点:基层员工,没有下级
|
||||
*
|
||||
* 权限要求:system:user-management-relation:query
|
||||
* 权限要求:仅需登录,不做权限码校验
|
||||
*
|
||||
* @return 用户管理链路树形列表
|
||||
*/
|
||||
@GetMapping("/tree")
|
||||
@Operation(summary = "获取用户管理链路树形结构", description = "用于前端树形控件展示,包含用户的上下级层级关系")
|
||||
@PreAuthorize("@ss.hasPermission('system:user-management-relation:query')")
|
||||
public CommonResult<List<UserManagementRelationTreeRespVO>> getUserManagementRelationTree(@Validated UserManagementRelationQueryReqVO reqVO) {
|
||||
return success(userManagementRelationService.getRelationTree(reqVO));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.njcn.rdms.module.system.controller.admin.user;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||
import com.njcn.rdms.module.system.controller.admin.user.vo.signature.UserSignatureRespVO;
|
||||
import com.njcn.rdms.module.system.controller.admin.user.vo.signature.UserSignatureSaveReqVO;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.user.UserSignatureDO;
|
||||
import com.njcn.rdms.module.system.service.user.UserSignatureService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
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.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
import static com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
||||
|
||||
@Tag(name = "管理后台 - 用户电子签名")
|
||||
@RestController
|
||||
@RequestMapping("/system/user-signatures")
|
||||
@Validated
|
||||
public class UserSignatureController {
|
||||
|
||||
@Resource
|
||||
private UserSignatureService userSignatureService;
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获取用户电子签名")
|
||||
@Parameter(name = "userId", description = "用户 ID", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('system:user-signature:query')")
|
||||
public CommonResult<UserSignatureRespVO> get(@RequestParam("userId") Long userId) {
|
||||
return success(toRespVO(userSignatureService.getByUserId(userId)));
|
||||
}
|
||||
|
||||
@PostMapping("/save")
|
||||
@Operation(summary = "保存用户电子签名")
|
||||
@PreAuthorize("@ss.hasPermission('system:user-signature:update')")
|
||||
public CommonResult<Boolean> save(@Valid @RequestBody UserSignatureSaveReqVO reqVO) {
|
||||
userSignatureService.save(reqVO.getUserId(), reqVO.getFileId(), reqVO.getFileName(), reqVO.getStatus(), reqVO.getRemark());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除用户电子签名")
|
||||
@Parameter(name = "userId", description = "用户 ID", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('system:user-signature:update')")
|
||||
public CommonResult<Boolean> delete(@RequestParam("userId") Long userId) {
|
||||
userSignatureService.deleteByUserId(userId);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/my")
|
||||
@Operation(summary = "获取当前登录用户电子签名")
|
||||
public CommonResult<UserSignatureRespVO> my() {
|
||||
return success(toRespVO(userSignatureService.getByUserId(getLoginUserId())));
|
||||
}
|
||||
|
||||
private UserSignatureRespVO toRespVO(UserSignatureDO signature) {
|
||||
return BeanUtils.toBean(signature, UserSignatureRespVO.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.njcn.rdms.module.system.controller.admin.user.vo.signature;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - 用户电子签名 Response VO")
|
||||
@Data
|
||||
public class UserSignatureRespVO {
|
||||
|
||||
private Long id;
|
||||
|
||||
private Long userId;
|
||||
|
||||
private Long fileId;
|
||||
|
||||
private String fileName;
|
||||
|
||||
private Integer status;
|
||||
|
||||
private String remark;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.njcn.rdms.module.system.controller.admin.user.vo.signature;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 用户电子签名保存 Request VO")
|
||||
@Data
|
||||
public class UserSignatureSaveReqVO {
|
||||
|
||||
@Schema(description = "用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "用户 ID 不能为空")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "电子签文件 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
|
||||
@NotNull(message = "电子签文件 ID 不能为空")
|
||||
private Long fileId;
|
||||
|
||||
@Schema(description = "文件名", example = "zhangsan-sign.png")
|
||||
@Size(max = 255, message = "文件名长度不能超过 255 个字符")
|
||||
private String fileName;
|
||||
|
||||
@Schema(description = "状态", example = "0")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "备注")
|
||||
@Size(max = 255, message = "备注长度不能超过 255 个字符")
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.njcn.rdms.module.system.dal.dataobject.user;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 用户电子签名。
|
||||
*/
|
||||
@TableName("system_user_signature")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class UserSignatureDO extends BaseDO {
|
||||
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
private Long userId;
|
||||
|
||||
private Long fileId;
|
||||
|
||||
private String fileName;
|
||||
|
||||
private Integer status;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.njcn.rdms.module.system.dal.mysql.user;
|
||||
|
||||
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.user.UserSignatureDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface UserSignatureMapper extends BaseMapperX<UserSignatureDO> {
|
||||
|
||||
default UserSignatureDO selectByUserId(Long userId) {
|
||||
return selectOne(new LambdaQueryWrapperX<UserSignatureDO>()
|
||||
.eq(UserSignatureDO::getUserId, userId));
|
||||
}
|
||||
|
||||
default int deleteByUserId(Long userId) {
|
||||
return delete(new LambdaQueryWrapperX<UserSignatureDO>()
|
||||
.eq(UserSignatureDO::getUserId, userId));
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import com.google.common.annotations.VisibleForTesting;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.framework.common.util.http.HttpUtils;
|
||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||
import com.njcn.rdms.framework.security.core.service.SecurityFrameworkService;
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.module.system.controller.admin.file.vo.file.FileCreateReqVO;
|
||||
import com.njcn.rdms.module.system.controller.admin.file.vo.file.FilePageReqVO;
|
||||
import com.njcn.rdms.module.system.controller.admin.file.vo.file.FilePresignedUrlRespVO;
|
||||
@@ -27,6 +29,7 @@ import java.util.List;
|
||||
|
||||
import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN;
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.FILE_DELETE_NO_PERMISSION;
|
||||
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.FILE_NOT_EXISTS;
|
||||
|
||||
/**
|
||||
@@ -52,6 +55,9 @@ public class FileServiceImpl implements FileService {
|
||||
@Resource
|
||||
private FileContentMapper fileContentMapper;
|
||||
|
||||
@Resource
|
||||
private SecurityFrameworkService securityFrameworkService;
|
||||
|
||||
@Override
|
||||
public PageResult<FileDO> getFilePage(FilePageReqVO pageReqVO) {
|
||||
return fileMapper.selectPage(pageReqVO);
|
||||
@@ -172,6 +178,8 @@ public class FileServiceImpl implements FileService {
|
||||
public void deleteFile(Long id) throws Exception {
|
||||
// 校验存在
|
||||
FileDO file = validateFileExists(id);
|
||||
// 校验删除权限:上传者本人,或具备文件删除权限者
|
||||
validateFileDeletePermission(file);
|
||||
|
||||
// 从文件存储器中删除
|
||||
FileClient client = fileConfigService.getFileClient(file.getConfigId());
|
||||
@@ -187,6 +195,8 @@ public class FileServiceImpl implements FileService {
|
||||
public void deleteFileList(List<Long> ids) {
|
||||
// 删除文件
|
||||
List<FileDO> files = fileMapper.selectByIds(ids);
|
||||
// 逐个校验删除权限,任一越权即整体拒绝
|
||||
files.forEach(this::validateFileDeletePermission);
|
||||
for (FileDO file : files) {
|
||||
// 获取客户端
|
||||
FileClient client = fileConfigService.getFileClient(file.getConfigId());
|
||||
@@ -207,6 +217,25 @@ public class FileServiceImpl implements FileService {
|
||||
return fileDO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验文件删除权限。
|
||||
* <p>
|
||||
* 口径:上传者本人可删除自己上传的文件;其余情况需具备文件管理删除权限 {@code system:file:delete}(运维 / 管理员)。
|
||||
* 该接口按 id 删除、不校验业务归属,故不能对所有登录用户放开,只对"本人上传"放行。
|
||||
*/
|
||||
private void validateFileDeletePermission(FileDO file) {
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
// 上传者本人放行(creator 存的是上传时的登录用户 id)
|
||||
if (loginUserId != null && String.valueOf(loginUserId).equals(file.getCreator())) {
|
||||
return;
|
||||
}
|
||||
// 兜底:具备文件删除权限者(运维 / 管理员)可删除任意文件
|
||||
if (securityFrameworkService.hasPermission("system:file:delete")) {
|
||||
return;
|
||||
}
|
||||
throw exception(FILE_DELETE_NO_PERMISSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getFileContent(Long configId, String path) throws Exception {
|
||||
// 软删 / 不存在的记录直接拒绝
|
||||
|
||||
@@ -563,8 +563,8 @@ public class AdminUserServiceImpl implements AdminUserService {
|
||||
|
||||
url = client.upload(content, path, type);
|
||||
|
||||
//删除旧的
|
||||
client.delete(user.getAvatar());
|
||||
// 旧头像删除失败不应影响新头像上传;而且 avatar 字段存的是 URL,不能直接当 path 删。
|
||||
deleteOldAvatarQuietly(client, user);
|
||||
|
||||
// 3. 保存到数据库
|
||||
fileDO = new FileDO().setConfigId(client.getId())
|
||||
@@ -574,11 +574,31 @@ public class AdminUserServiceImpl implements AdminUserService {
|
||||
user.setAvatar(fileDO.getUrl());
|
||||
this.userMapper.updateById(user);
|
||||
} catch (Exception e) {
|
||||
log.warn("头像上传失败,userId={}, avatar={}, fileName={}, contentType={}",
|
||||
user == null ? null : user.getId(),
|
||||
user == null ? null : user.getAvatar(),
|
||||
name, type, e);
|
||||
throw exception(USER_AVATAR_UPLOAD_FAILED);
|
||||
}
|
||||
return fileDO;
|
||||
}
|
||||
|
||||
private void deleteOldAvatarQuietly(FileClient client, AdminUserDO user) {
|
||||
if (client == null || user == null || StrUtil.isBlank(user.getAvatar())) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
FileDO oldAvatar = fileMapper.selectByUrl(user.getAvatar());
|
||||
if (oldAvatar == null || StrUtil.isBlank(oldAvatar.getPath())) {
|
||||
log.warn("删除旧头像时未找到文件记录,userId={}, avatar={}", user.getId(), user.getAvatar());
|
||||
return;
|
||||
}
|
||||
client.delete(oldAvatar.getPath());
|
||||
} catch (Exception ex) {
|
||||
log.warn("删除旧头像失败,userId={}, avatar={}", user.getId(), user.getAvatar(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateImageFile(MultipartFile file) {
|
||||
// 检查文件类型
|
||||
String contentType = file.getContentType();
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.njcn.rdms.module.system.service.user;
|
||||
|
||||
import com.njcn.rdms.module.system.dal.dataobject.user.UserSignatureDO;
|
||||
|
||||
public interface UserSignatureService {
|
||||
|
||||
UserSignatureDO getByUserId(Long userId);
|
||||
|
||||
void save(Long userId, Long fileId, String fileName, Integer status, String remark);
|
||||
|
||||
void deleteByUserId(Long userId);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.njcn.rdms.module.system.service.user;
|
||||
|
||||
import com.njcn.rdms.framework.common.enums.CommonStatusEnum;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.file.FileDO;
|
||||
import com.njcn.rdms.module.system.dal.dataobject.user.UserSignatureDO;
|
||||
import com.njcn.rdms.module.system.dal.mysql.user.UserSignatureMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static com.njcn.rdms.framework.common.util.collection.CollectionUtils.singleton;
|
||||
|
||||
@Service
|
||||
public class UserSignatureServiceImpl implements UserSignatureService {
|
||||
|
||||
@Resource
|
||||
private UserSignatureMapper userSignatureMapper;
|
||||
@Resource
|
||||
private AdminUserService adminUserService;
|
||||
@Resource
|
||||
private com.njcn.rdms.module.system.service.file.FileService fileService;
|
||||
|
||||
@Override
|
||||
public UserSignatureDO getByUserId(Long userId) {
|
||||
if (userId == null) {
|
||||
return null;
|
||||
}
|
||||
return userSignatureMapper.selectByUserId(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void save(Long userId, Long fileId, String fileName, Integer status, String remark) {
|
||||
adminUserService.validateUserList(singleton(userId));
|
||||
FileDO file = fileService.getFile(fileId);
|
||||
|
||||
UserSignatureDO existing = userSignatureMapper.selectByUserId(userId);
|
||||
UserSignatureDO target = new UserSignatureDO();
|
||||
if (existing != null) {
|
||||
target.setId(existing.getId());
|
||||
}
|
||||
target.setUserId(userId);
|
||||
target.setFileId(fileId);
|
||||
target.setFileName(StringUtils.hasText(fileName) ? fileName.trim() : file.getName());
|
||||
target.setStatus(status == null ? CommonStatusEnum.ENABLE.getStatus() : status);
|
||||
target.setRemark(StringUtils.hasText(remark) ? remark.trim() : null);
|
||||
if (existing == null) {
|
||||
userSignatureMapper.insert(target);
|
||||
} else {
|
||||
userSignatureMapper.updateById(target);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteByUserId(Long userId) {
|
||||
if (userId == null) {
|
||||
return;
|
||||
}
|
||||
userSignatureMapper.deleteByUserId(userId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user