feat(personal-item): 个人事项
This commit is contained in:
@@ -202,4 +202,15 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PROJECT_REQUIREMENT_MODULE_HAS_REQUIREMENTS = new ErrorCode(1_008_007_016, "模块下存在项目需求,请先删除需求");
|
||||
ErrorCode PROJECT_REQUIREMENT_CHILD_NOT_ALLOW_CANCEL = new ErrorCode(1_008_007_017, "只有不存在子需求,或子需求都处于已取消和已拒绝状态时,父需求才允许取消");
|
||||
ErrorCode PROJECT_REQUIREMENT_SYNCED_FROM_PRODUCT_NOT_ALLOW_CANCEL = new ErrorCode(1_008_007_019, "\u7531\u4ea7\u54c1\u9700\u6c42\u6d41\u8f6c\u751f\u6210\u7684\u9879\u76ee\u9700\u6c42\u4e0d\u5141\u8bb8\u53d6\u6d88");
|
||||
|
||||
// ========== 个人事项 1_008_008_xxx ==========
|
||||
ErrorCode PERSONAL_ITEM_NOT_EXISTS = new ErrorCode(1_008_008_001, "个人事项不存在");
|
||||
ErrorCode PERSONAL_ITEM_OWNER_NOT_IN_EXECUTION = new ErrorCode(1_008_008_002, "个人事项负责人必须属于当前有效执行团队成员");
|
||||
ErrorCode PERSONAL_ITEM_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_008_003, "个人事项状态定义不存在或已停用");
|
||||
ErrorCode PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_008_004, "当前个人事项状态不支持动作【{}】");
|
||||
ErrorCode PERSONAL_ITEM_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_008_005, "动作【{}】必须填写原因");
|
||||
ErrorCode PERSONAL_ITEM_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_008_006, "个人事项状态已发生变化,请刷新后重试");
|
||||
ErrorCode PERSONAL_ITEM_STATUS_NOT_ALLOW_EDIT = new ErrorCode(1_008_008_007, "当前个人事项状态不允许编辑");
|
||||
ErrorCode PERSONAL_ITEM_NOT_ALLOW_DELETE = new ErrorCode(1_008_008_008, "仅初始态(待开始)的个人事项允许删除");
|
||||
ErrorCode PERSONAL_ITEM_WRITE_FORBIDDEN = new ErrorCode(1_008_008_009, "无权修改个人事项");
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@ public final class ObjectActivityConstants {
|
||||
public static final String TASK_ACTION_CREATE = "create_task_entity";
|
||||
public static final String TASK_ACTION_UPDATE = "update_task_entity";
|
||||
public static final String TASK_ACTION_DELETE = "delete_task_entity";
|
||||
public static final String PERSONAL_ITEM_ACTION_CREATE = "create_personal_item";
|
||||
public static final String PERSONAL_ITEM_ACTION_UPDATE = "update_personal_item";
|
||||
public static final String PERSONAL_ITEM_ACTION_DELETE = "delete_personal_item";
|
||||
|
||||
// ========== 任务协办人事件类型(B 模型 - 多行周期记录) ==========
|
||||
public static final String TASK_ASSIGNEE_ACTION_JOIN = "join";
|
||||
@@ -134,6 +137,9 @@ public final class ObjectActivityConstants {
|
||||
case TASK_ACTION_CREATE -> "创建任务";
|
||||
case TASK_ACTION_UPDATE -> "更新任务";
|
||||
case TASK_ACTION_DELETE -> "删除任务";
|
||||
case PERSONAL_ITEM_ACTION_CREATE -> "创建个人事项";
|
||||
case PERSONAL_ITEM_ACTION_UPDATE -> "更新个人事项";
|
||||
case PERSONAL_ITEM_ACTION_DELETE -> "删除个人事项";
|
||||
case TASK_ASSIGNEE_ACTION_JOIN -> "加入";
|
||||
case TASK_ASSIGNEE_ACTION_INACTIVE -> "退出";
|
||||
case EXECUTION_ASSIGNEE_LOG_ACTION_OWNER_TRANSFER_IN -> "转入负责人";
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.njcn.rdms.module.project.constant;
|
||||
|
||||
/**
|
||||
* 个人事项常量。
|
||||
*/
|
||||
public final class PersonalItemConstants {
|
||||
|
||||
private PersonalItemConstants() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人事项状态模型复用任务对象类型。
|
||||
*/
|
||||
public static final String STATUS_OBJECT_TYPE = ProjectTaskConstants.OBJECT_TYPE;
|
||||
|
||||
/**
|
||||
* 个人事项业务类型。
|
||||
*/
|
||||
public static final String BIZ_TYPE = "personal_item";
|
||||
|
||||
public static final String PERMISSION_QUERY = "project:personal-item:query";
|
||||
public static final String PERMISSION_CREATE = "project:personal-item:create";
|
||||
public static final String PERMISSION_UPDATE = "project:personal-item:update";
|
||||
public static final String PERMISSION_DELETE = "project:personal-item:delete";
|
||||
public static final String PERMISSION_STATUS = "project:personal-item:status";
|
||||
|
||||
public static final String STATUS_COMPLETED = "completed";
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.personal;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.module.project.constant.PersonalItemConstants;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemSaveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogSaveReqVO;
|
||||
import com.njcn.rdms.module.project.service.personal.PersonalItemService;
|
||||
import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - 个人事项")
|
||||
@RestController
|
||||
@RequestMapping("/project/personal-items")
|
||||
@Validated
|
||||
public class PersonalItemController {
|
||||
|
||||
@Resource
|
||||
private PersonalItemService personalItemService;
|
||||
@Resource
|
||||
private ProjectExecutionService projectExecutionService;
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "创建个人事项")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_CREATE + "')")
|
||||
public CommonResult<Long> create(@Valid @RequestBody PersonalItemSaveReqVO reqVO) {
|
||||
return success(personalItemService.createItem(reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@Operation(summary = "更新个人事项")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||
public CommonResult<Boolean> update(@PathVariable("id") Long id,
|
||||
@Valid @RequestBody PersonalItemSaveReqVO reqVO) {
|
||||
personalItemService.updateItem(id, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "获取个人事项详情")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
||||
public CommonResult<PersonalItemRespVO> get(@PathVariable("id") Long id) {
|
||||
return success(personalItemService.getItemRespVO(id));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获取个人事项分页")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
||||
public CommonResult<PageResult<PersonalItemRespVO>> page(@Valid PersonalItemPageReqVO reqVO) {
|
||||
return success(personalItemService.getItemRespVOPage(reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/change-status")
|
||||
@Operation(summary = "变更个人事项状态")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_STATUS + "')")
|
||||
public CommonResult<Boolean> changeStatus(@PathVariable("id") Long id,
|
||||
@Valid @RequestBody PersonalItemStatusActionReqVO reqVO) {
|
||||
personalItemService.changeStatus(id, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/worklogs")
|
||||
@Operation(summary = "新增个人事项工作日志")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||
public CommonResult<Long> createWorklog(@PathVariable("id") Long id,
|
||||
@Valid @RequestBody TaskWorklogSaveReqVO reqVO) {
|
||||
return success(personalItemService.createWorklog(id, reqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/worklogs")
|
||||
@Operation(summary = "获取个人事项工作日志分页")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_QUERY + "')")
|
||||
public CommonResult<PageResult<TaskWorklogRespVO>> getWorklogPage(@PathVariable("id") Long id,
|
||||
@Valid TaskWorklogPageReqVO reqVO) {
|
||||
return success(personalItemService.getWorklogPage(id, reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/worklogs/{worklogId}")
|
||||
@Operation(summary = "修改个人事项工作日志")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||
public CommonResult<Boolean> updateWorklog(@PathVariable("id") Long id,
|
||||
@PathVariable("worklogId") Long worklogId,
|
||||
@Valid @RequestBody TaskWorklogSaveReqVO reqVO) {
|
||||
personalItemService.updateWorklog(id, worklogId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}/worklogs/{worklogId}")
|
||||
@Operation(summary = "删除个人事项工作日志")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||
public CommonResult<Boolean> deleteWorklog(@PathVariable("id") Long id,
|
||||
@PathVariable("worklogId") Long worklogId) {
|
||||
personalItemService.deleteWorklog(id, worklogId);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}/worklogs/delete-list")
|
||||
@Operation(summary = "批量删除个人事项工作日志")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||
public CommonResult<Boolean> deleteWorklogs(@PathVariable("id") Long id,
|
||||
@RequestParam("ids") List<Long> ids) {
|
||||
personalItemService.deleteWorklogs(id, ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除个人事项")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_DELETE + "')")
|
||||
public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
|
||||
personalItemService.deleteItem(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete-list")
|
||||
@Operation(summary = "批量删除个人事项")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_DELETE + "')")
|
||||
public CommonResult<Boolean> deleteList(@RequestParam("ids") List<Long> ids) {
|
||||
personalItemService.deleteItems(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/relate-execution")
|
||||
@Operation(summary = "批量个人事项关联执行")
|
||||
@PreAuthorize("@ss.hasPermission('" + PersonalItemConstants.PERMISSION_UPDATE + "')")
|
||||
public CommonResult<Boolean> relateExecution(@RequestParam("itemIds") List<Long> itemIds,
|
||||
@RequestParam("executionId") Long executionId) {
|
||||
personalItemService.relateExecution(itemIds, executionId);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/owner/all-execution")
|
||||
@Operation(summary = "获取当前登录用户负责的所有执行")
|
||||
public CommonResult<List<ProjectExecutionRespVO>> getCurrentUserExecutionList() {
|
||||
return success(projectExecutionService.getCurrentUserExecutionList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.personal.vo.item;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Schema(description = "管理后台 - 个人事项生命周期动作 Response VO")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class PersonalItemLifecycleActionRespVO {
|
||||
|
||||
@Schema(description = "动作编码", example = "complete")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "动作名称", example = "完成")
|
||||
private String actionName;
|
||||
|
||||
@Schema(description = "是否必须填写原因", example = "true")
|
||||
private Boolean needReason;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.personal.vo.item;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - 个人事项分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PersonalItemPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "关键词,匹配个人事项标题", example = "沟通纪要")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "负责人用户编号;当前阶段仅支持查询本人事项", example = "3001")
|
||||
private Long ownerId;
|
||||
|
||||
@Schema(description = "个人事项状态编码", example = "pending")
|
||||
@Size(max = 32, message = "个人事项状态编码长度不能超过32个字符")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "更新时间", example = "[2026-05-01 00:00:00, 2026-05-31 23:59:59]")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] updateTime;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.personal.vo.item;
|
||||
|
||||
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 个人事项 Response VO")
|
||||
@Data
|
||||
public class PersonalItemRespVO {
|
||||
|
||||
@Schema(description = "个人事项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9001")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "个人事项标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "整理供应商沟通纪要")
|
||||
private String taskTitle;
|
||||
|
||||
@Schema(description = "负责人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3001")
|
||||
private Long ownerId;
|
||||
|
||||
@Schema(description = "负责人用户昵称", example = "小王")
|
||||
private String ownerNickname;
|
||||
|
||||
@Schema(description = "个人事项状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "个人事项状态名称", example = "待开始")
|
||||
private String statusName;
|
||||
|
||||
@Schema(description = "是否终态", example = "false")
|
||||
private Boolean terminal;
|
||||
|
||||
@Schema(description = "当前状态是否允许编辑", example = "true")
|
||||
private Boolean allowEdit;
|
||||
|
||||
@Schema(description = "当前状态可执行动作")
|
||||
private List<PersonalItemLifecycleActionRespVO> availableActions;
|
||||
|
||||
@Schema(description = "个人事项进度", example = "60.00")
|
||||
private BigDecimal progressRate;
|
||||
|
||||
@Schema(description = "已填报工时合计(小时,0.5 颗粒);逻辑删除的工时记录不计入。无记录默认为 0",
|
||||
example = "8.0")
|
||||
private BigDecimal totalSpentHours;
|
||||
|
||||
@Schema(description = "计划开始日期")
|
||||
private LocalDate plannedStartDate;
|
||||
|
||||
@Schema(description = "计划结束日期")
|
||||
private LocalDate plannedEndDate;
|
||||
|
||||
@Schema(description = "实际开始日期")
|
||||
private LocalDate actualStartDate;
|
||||
|
||||
@Schema(description = "实际结束日期")
|
||||
private LocalDate actualEndDate;
|
||||
|
||||
@Schema(description = "个人事项说明")
|
||||
private String taskDesc;
|
||||
|
||||
@Schema(description = "最近一次状态动作原因")
|
||||
private String lastStatusReason;
|
||||
|
||||
@Schema(description = "附件列表")
|
||||
private List<AttachmentItem> attachments;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.personal.vo.item;
|
||||
|
||||
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.DecimalMax;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 个人事项保存 Request VO")
|
||||
@Data
|
||||
public class PersonalItemSaveReqVO {
|
||||
|
||||
@Schema(description = "执行编号,仅用于创建/编辑时补充执行成员合法性校验", example = "5001")
|
||||
private Long executionId;
|
||||
|
||||
@Schema(description = "个人事项标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "整理供应商沟通纪要")
|
||||
@NotBlank(message = "个人事项标题不能为空")
|
||||
@Size(max = 300, message = "个人事项标题长度不能超过300个字符")
|
||||
private String taskTitle;
|
||||
|
||||
@Schema(description = "个人事项进度(0~100)", example = "60.00")
|
||||
@DecimalMin(value = "0.00", message = "个人事项进度不能小于 0")
|
||||
@DecimalMax(value = "100.00", message = "个人事项进度不能大于 100")
|
||||
private BigDecimal progressRate;
|
||||
|
||||
@Schema(description = "计划开始日期", example = "2026-05-20")
|
||||
private LocalDate plannedStartDate;
|
||||
|
||||
@Schema(description = "计划结束日期", example = "2026-05-25")
|
||||
private LocalDate plannedEndDate;
|
||||
|
||||
@Schema(description = "个人事项说明(富文本 HTML)")
|
||||
private String taskDesc;
|
||||
|
||||
@Schema(description = "附件列表")
|
||||
@Valid
|
||||
private List<AttachmentItem> attachments;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.personal.vo.item;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 个人事项状态动作 Request VO")
|
||||
@Data
|
||||
public class PersonalItemStatusActionReqVO {
|
||||
|
||||
@Schema(description = "动作编码,如 start、complete、reopen", requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
example = "complete")
|
||||
@NotBlank(message = "动作编码不能为空")
|
||||
@Size(max = 32, message = "动作编码长度不能超过32个字符")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "动作原因;是否必填由状态流转配置决定", example = "事项完成")
|
||||
@Size(max = 500, message = "动作原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
}
|
||||
@@ -20,6 +20,8 @@ import jakarta.validation.Valid;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - 项目执行")
|
||||
@@ -33,6 +35,7 @@ public class ProjectExecutionController {
|
||||
@Resource
|
||||
private ProjectStatusBoardService projectStatusBoardService;
|
||||
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "创建执行")
|
||||
public CommonResult<Long> createExecution(@PathVariable("projectId") Long projectId,
|
||||
|
||||
@@ -54,4 +54,6 @@ public class TaskWorklogSaveReqVO {
|
||||
@Valid
|
||||
private List<AttachmentItem> attachments;
|
||||
|
||||
@Schema(description = "任务难度", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
|
||||
private String difficulty;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.personal;
|
||||
|
||||
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.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.attachment.AttachmentItem;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 个人事项主表。
|
||||
*/
|
||||
@TableName(value = "rdms_personal_item", autoResultMap = true)
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PersonalItemDO extends BaseDO {
|
||||
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
private String taskTitle;
|
||||
|
||||
private Long ownerId;
|
||||
|
||||
private String statusCode;
|
||||
|
||||
private BigDecimal progressRate;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private LocalDate plannedStartDate;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private LocalDate plannedEndDate;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private LocalDate actualStartDate;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private LocalDate actualEndDate;
|
||||
|
||||
@TableField(value = "task_desc", updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String taskDesc;
|
||||
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String lastStatusReason;
|
||||
|
||||
@TableField(typeHandler = JacksonTypeHandler.class, updateStrategy = FieldStrategy.ALWAYS)
|
||||
private List<AttachmentItem> attachments;
|
||||
}
|
||||
@@ -71,4 +71,5 @@ public class TaskWorklogDO extends BaseDO {
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<AttachmentItem> attachments;
|
||||
|
||||
private String difficulty;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.personal;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemPageReqVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.personal.PersonalItemDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@Mapper
|
||||
public interface PersonalItemMapper extends BaseMapperX<PersonalItemDO> {
|
||||
|
||||
default PersonalItemDO selectByIdAndOwnerId(Long id, Long ownerId) {
|
||||
return selectOne(new LambdaQueryWrapperX<PersonalItemDO>()
|
||||
.eq(PersonalItemDO::getId, id)
|
||||
.eq(PersonalItemDO::getOwnerId, ownerId));
|
||||
}
|
||||
|
||||
default PageResult<PersonalItemDO> selectPageByOwnerId(Long ownerId, PersonalItemPageReqVO reqVO) {
|
||||
LambdaQueryWrapperX<PersonalItemDO> queryWrapper = new LambdaQueryWrapperX<>();
|
||||
queryWrapper.eq(PersonalItemDO::getOwnerId, ownerId);
|
||||
queryWrapper.eqIfPresent(PersonalItemDO::getStatusCode, reqVO.getStatusCode());
|
||||
queryWrapper.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime());
|
||||
queryWrapper.orderByDesc(BaseDO::getUpdateTime);
|
||||
queryWrapper.orderByDesc(PersonalItemDO::getId);
|
||||
if (StringUtils.hasText(reqVO.getKeyword())) {
|
||||
queryWrapper.and(wrapper -> wrapper.like(PersonalItemDO::getTaskTitle, reqVO.getKeyword()));
|
||||
}
|
||||
return selectPage(reqVO, queryWrapper);
|
||||
}
|
||||
|
||||
default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) {
|
||||
PersonalItemDO update = new PersonalItemDO();
|
||||
update.setStatusCode(toStatus);
|
||||
update.setLastStatusReason(lastStatusReason);
|
||||
return update(update, new LambdaQueryWrapperX<PersonalItemDO>()
|
||||
.eq(PersonalItemDO::getId, id)
|
||||
.eq(PersonalItemDO::getStatusCode, fromStatus));
|
||||
}
|
||||
|
||||
default int deleteByIdAndStatus(Long id, String fromStatus) {
|
||||
return delete(new LambdaQueryWrapperX<PersonalItemDO>()
|
||||
.eq(PersonalItemDO::getId, id)
|
||||
.eq(PersonalItemDO::getStatusCode, fromStatus));
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,13 @@ public interface ProjectExecutionMapper extends BaseMapperX<ProjectExecutionDO>
|
||||
.toList();
|
||||
}
|
||||
|
||||
default List<ProjectExecutionDO> selectListByOwnerId(Long ownerId) {
|
||||
return selectList(new LambdaQueryWrapperX<ProjectExecutionDO>()
|
||||
.eq(ProjectExecutionDO::getOwnerId, ownerId)
|
||||
.orderByDesc(BaseDO::getCreateTime)
|
||||
.orderByDesc(ProjectExecutionDO::getId));
|
||||
}
|
||||
|
||||
default Integer countNonTerminalByProjectIdAndOwnerId(Long projectId, Long ownerId, List<String> terminalStatusCodes) {
|
||||
LambdaQueryWrapperX<ProjectExecutionDO> queryWrapper = new LambdaQueryWrapperX<ProjectExecutionDO>()
|
||||
.eq(ProjectExecutionDO::getProjectId, projectId)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.njcn.rdms.module.project.service.personal;
|
||||
|
||||
import com.njcn.rdms.module.project.dal.dataobject.personal.PersonalItemDO;
|
||||
|
||||
public interface PersonalItemAccessService {
|
||||
|
||||
void checkCanWrite(PersonalItemDO item);
|
||||
|
||||
void validateOwnerInExecution(Long executionId, Long ownerId);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.njcn.rdms.module.project.service.personal;
|
||||
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.personal.PersonalItemDO;
|
||||
import com.njcn.rdms.module.project.dal.mysql.project.execution.ExecutionAssigneeMapper;
|
||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
|
||||
@Service
|
||||
public class PersonalItemAccessServiceImpl implements PersonalItemAccessService {
|
||||
|
||||
@Resource
|
||||
private ExecutionAssigneeMapper executionAssigneeMapper;
|
||||
|
||||
@Override
|
||||
public void checkCanWrite(PersonalItemDO item) {
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
if (loginUserId == null || item == null || !Objects.equals(loginUserId, item.getOwnerId())) {
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_WRITE_FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateOwnerInExecution(Long executionId, Long ownerId) {
|
||||
if (executionId == null || ownerId == null
|
||||
|| executionAssigneeMapper.selectActiveByExecutionIdAndUserId(executionId, ownerId) == null) {
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_OWNER_NOT_IN_EXECUTION);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.njcn.rdms.module.project.service.personal;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemSaveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogSaveReqVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PersonalItemService {
|
||||
|
||||
Long createItem(PersonalItemSaveReqVO reqVO);
|
||||
|
||||
void updateItem(Long id, PersonalItemSaveReqVO reqVO);
|
||||
|
||||
PersonalItemRespVO getItemRespVO(Long id);
|
||||
|
||||
PageResult<PersonalItemRespVO> getItemRespVOPage(PersonalItemPageReqVO reqVO);
|
||||
|
||||
void changeStatus(Long id, PersonalItemStatusActionReqVO reqVO);
|
||||
|
||||
Long createWorklog(Long id, TaskWorklogSaveReqVO reqVO);
|
||||
|
||||
PageResult<TaskWorklogRespVO> getWorklogPage(Long id, TaskWorklogPageReqVO reqVO);
|
||||
|
||||
void updateWorklog(Long id, Long worklogId, TaskWorklogSaveReqVO reqVO);
|
||||
|
||||
void deleteWorklog(Long id, Long worklogId);
|
||||
|
||||
void deleteWorklogs(Long id, List<Long> ids);
|
||||
|
||||
void deleteItem(Long id);
|
||||
|
||||
void deleteItems(List<Long> ids);
|
||||
|
||||
void relateExecution(List<Long> itemIds, Long executionId);
|
||||
}
|
||||
@@ -0,0 +1,745 @@
|
||||
package com.njcn.rdms.module.project.service.personal;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.framework.common.util.json.JsonUtils;
|
||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
|
||||
import com.njcn.rdms.module.project.constant.PersonalItemConstants;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemSaveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogSaveReqVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.personal.PersonalItemDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskWorklogDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
|
||||
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.personal.PersonalItemMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.project.task.TaskWorklogMapper;
|
||||
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.framework.attachment.AttachmentFileIdResolver;
|
||||
import com.njcn.rdms.module.project.framework.attachment.AttachmentValidator;
|
||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
|
||||
@Service
|
||||
public class PersonalItemServiceImpl implements PersonalItemService {
|
||||
|
||||
private static final BigDecimal DURATION_GRANULARITY_HOURS = new BigDecimal("0.5");
|
||||
private static final String WORKLOG_AUTO_START_ACTION = ObjectActivityConstants.TASK_ACTION_AUTO_START;
|
||||
private static final String WORKLOG_AUTO_START_REASON = "由工作日志自动触发";
|
||||
|
||||
@Resource
|
||||
private PersonalItemMapper personalItemMapper;
|
||||
@Resource
|
||||
private ProjectExecutionMapper projectExecutionMapper;
|
||||
@Resource
|
||||
private ProjectTaskMapper projectTaskMapper;
|
||||
@Resource
|
||||
private BizAuditLogMapper bizAuditLogMapper;
|
||||
@Resource
|
||||
private ObjectStatusModelMapper objectStatusModelMapper;
|
||||
@Resource
|
||||
private ObjectStatusTransitionMapper objectStatusTransitionMapper;
|
||||
@Resource
|
||||
private TaskWorklogMapper taskWorklogMapper;
|
||||
@Resource
|
||||
private PersonalItemAccessService personalItemAccessService;
|
||||
@Resource
|
||||
private PersonalItemStatusViewService personalItemStatusViewService;
|
||||
@Resource
|
||||
private AttachmentFileIdResolver attachmentFileIdResolver;
|
||||
@Resource
|
||||
private AdminUserApi adminUserApi;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createItem(PersonalItemSaveReqVO reqVO) {
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
validateDateRange(reqVO.getPlannedStartDate(), reqVO.getPlannedEndDate(), "计划结束日期不能早于计划开始日期");
|
||||
AttachmentValidator.validate(reqVO.getAttachments());
|
||||
attachmentFileIdResolver.resolve(reqVO.getAttachments());
|
||||
|
||||
PersonalItemDO item = new PersonalItemDO();
|
||||
item.setTaskTitle(normalizeRequiredTitle(reqVO.getTaskTitle()));
|
||||
item.setOwnerId(loginUserId);
|
||||
item.setStatusCode(getInitialStatusCode());
|
||||
item.setProgressRate(normalizeProgress(reqVO.getProgressRate()));
|
||||
item.setPlannedStartDate(reqVO.getPlannedStartDate());
|
||||
item.setPlannedEndDate(reqVO.getPlannedEndDate());
|
||||
item.setTaskDesc(normalizeNullableText(reqVO.getTaskDesc()));
|
||||
item.setAttachments(reqVO.getAttachments());
|
||||
personalItemMapper.insert(item);
|
||||
|
||||
writeAuditLog(item, ObjectActivityConstants.PERSONAL_ITEM_ACTION_CREATE, null,
|
||||
item.getStatusCode(), buildFieldChanges(null, item), null);
|
||||
return item.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateItem(Long id, PersonalItemSaveReqVO reqVO) {
|
||||
PersonalItemDO item = validateItemExists(id);
|
||||
personalItemAccessService.checkCanWrite(item);
|
||||
validateItemAllowEdit(item);
|
||||
// if (reqVO.getExecutionId() != null) {
|
||||
// personalItemAccessService.validateOwnerInExecution(reqVO.getExecutionId(), item.getOwnerId());
|
||||
// }
|
||||
validateDateRange(reqVO.getPlannedStartDate(), reqVO.getPlannedEndDate(), "计划结束日期不能早于计划开始日期");
|
||||
AttachmentValidator.validate(reqVO.getAttachments());
|
||||
attachmentFileIdResolver.resolve(reqVO.getAttachments());
|
||||
|
||||
PersonalItemDO before = cloneItem(item);
|
||||
item.setTaskTitle(normalizeRequiredTitle(reqVO.getTaskTitle()));
|
||||
item.setProgressRate(normalizeProgress(reqVO.getProgressRate()));
|
||||
item.setPlannedStartDate(reqVO.getPlannedStartDate());
|
||||
item.setPlannedEndDate(reqVO.getPlannedEndDate());
|
||||
item.setTaskDesc(normalizeNullableText(reqVO.getTaskDesc()));
|
||||
item.setAttachments(reqVO.getAttachments());
|
||||
personalItemMapper.updateById(item);
|
||||
|
||||
writeAuditLog(item, ObjectActivityConstants.PERSONAL_ITEM_ACTION_UPDATE, before.getStatusCode(),
|
||||
item.getStatusCode(), buildFieldChanges(before, item), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PersonalItemRespVO getItemRespVO(Long id) {
|
||||
PersonalItemDO item = validateOwnedItemExists(id);
|
||||
PersonalItemRespVO respVO = toRespVO(item);
|
||||
respVO.setOwnerNickname(loadUserNickname(item.getOwnerId()));
|
||||
respVO.setTotalSpentHours(defaultDuration(taskWorklogMapper.sumDurationByTaskId(item.getId())));
|
||||
return respVO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<PersonalItemRespVO> getItemRespVOPage(PersonalItemPageReqVO reqVO) {
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
if (reqVO.getOwnerId() != null && !Objects.equals(reqVO.getOwnerId(), loginUserId)) {
|
||||
return PageResult.empty();
|
||||
}
|
||||
reqVO.setOwnerId(loginUserId);
|
||||
PageResult<PersonalItemDO> doPage = personalItemMapper.selectPageByOwnerId(loginUserId, reqVO);
|
||||
List<PersonalItemRespVO> list = doPage.getList().stream().map(this::toRespVO).toList();
|
||||
fillOwnerNicknames(list);
|
||||
fillTotalSpentHours(list);
|
||||
return new PageResult<>(list, doPage.getTotal());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void changeStatus(Long id, PersonalItemStatusActionReqVO reqVO) {
|
||||
PersonalItemDO item = validateItemExists(id);
|
||||
personalItemAccessService.checkCanWrite(item);
|
||||
|
||||
String actionCode = normalizeRequiredActionCode(reqVO.getActionCode());
|
||||
String fromStatus = item.getStatusCode();
|
||||
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
||||
.selectByObjectTypeAndFromStatusAndAction(PersonalItemConstants.STATUS_OBJECT_TYPE, fromStatus, actionCode);
|
||||
if (transition == null) {
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
||||
}
|
||||
String reason = normalizeNullableText(reqVO.getReason());
|
||||
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_ACTION_REASON_REQUIRED, actionCode);
|
||||
}
|
||||
String toStatus = transition.getToStatusCode();
|
||||
int updateCount = personalItemMapper.updateStatusByIdAndStatus(item.getId(), fromStatus, toStatus, reason);
|
||||
if (updateCount != 1) {
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_CONCURRENT_MODIFIED);
|
||||
}
|
||||
|
||||
PersonalItemDO before = cloneItem(item);
|
||||
item.setStatusCode(toStatus);
|
||||
item.setLastStatusReason(reason);
|
||||
applyLifecycleFields(item, fromStatus, toStatus);
|
||||
personalItemMapper.updateById(item);
|
||||
|
||||
writeAuditLog(item, actionCode, fromStatus, toStatus, buildFieldChanges(before, item), reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createWorklog(Long id, TaskWorklogSaveReqVO reqVO) {
|
||||
PersonalItemDO item = validateItemExists(id);
|
||||
personalItemAccessService.checkCanWrite(item);
|
||||
validateItemAllowEdit(item);
|
||||
|
||||
Long userId = item.getOwnerId();
|
||||
validateDateRange(reqVO.getStartDate(), reqVO.getEndDate(), "段起始日期不能晚于段结束日期");
|
||||
validateDurationGranularity(reqVO.getDurationHours());
|
||||
validateNoOverlap(item.getId(), userId, reqVO.getStartDate(), reqVO.getEndDate(), null);
|
||||
validateProgressMonotonicity(item.getId(), userId, reqVO.getEndDate(), reqVO.getProgressRate(), null);
|
||||
AttachmentValidator.validate(reqVO.getAttachments());
|
||||
attachmentFileIdResolver.resolve(reqVO.getAttachments());
|
||||
|
||||
TaskWorklogDO worklog = buildWorklog(item.getId(), userId, reqVO);
|
||||
taskWorklogMapper.insert(worklog);
|
||||
|
||||
PersonalItemDO current = cloneItem(item);
|
||||
maybeAutoStartByWorklog(current);
|
||||
syncOwnerProgressFromLatestWorklog(current);
|
||||
personalItemMapper.updateById(current);
|
||||
return worklog.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<TaskWorklogRespVO> getWorklogPage(Long id, TaskWorklogPageReqVO reqVO) {
|
||||
PersonalItemDO item = validateOwnedItemExists(id);
|
||||
if (reqVO.getUserId() != null && !Objects.equals(reqVO.getUserId(), item.getOwnerId())) {
|
||||
return PageResult.empty();
|
||||
}
|
||||
reqVO.setUserId(item.getOwnerId());
|
||||
PageResult<TaskWorklogDO> page = taskWorklogMapper.selectPageByTaskId(item.getId(), reqVO);
|
||||
if (page.getList().isEmpty()) {
|
||||
return new PageResult<>(Collections.emptyList(), page.getTotal());
|
||||
}
|
||||
Map<Long, String> nicknameMap = loadUserNicknameMap(page.getList().stream()
|
||||
.map(TaskWorklogDO::getUserId)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)));
|
||||
List<TaskWorklogRespVO> list = page.getList().stream().map(worklog -> {
|
||||
TaskWorklogRespVO vo = BeanUtils.toBean(worklog, TaskWorklogRespVO.class);
|
||||
vo.setUserNickname(nicknameMap.get(worklog.getUserId()));
|
||||
return vo;
|
||||
}).toList();
|
||||
return new PageResult<>(list, page.getTotal());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateWorklog(Long id, Long worklogId, TaskWorklogSaveReqVO reqVO) {
|
||||
PersonalItemDO item = validateItemExists(id);
|
||||
personalItemAccessService.checkCanWrite(item);
|
||||
validateItemAllowEdit(item);
|
||||
|
||||
TaskWorklogDO worklog = loadWorklog(worklogId, item.getId());
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
if (!Objects.equals(worklog.getUserId(), loginUserId)) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_EDIT_NOT_OWN);
|
||||
}
|
||||
validateDateRange(reqVO.getStartDate(), reqVO.getEndDate(), "娈佃捣濮嬫棩鏈熶笉鑳芥櫄浜庢缁撴潫鏃ユ湡");
|
||||
validateDurationGranularity(reqVO.getDurationHours());
|
||||
validateNoOverlap(item.getId(), worklog.getUserId(), reqVO.getStartDate(), reqVO.getEndDate(), worklog.getId());
|
||||
validateProgressMonotonicity(item.getId(), worklog.getUserId(), reqVO.getEndDate(), reqVO.getProgressRate(),
|
||||
worklog.getId());
|
||||
AttachmentValidator.validate(reqVO.getAttachments());
|
||||
attachmentFileIdResolver.resolve(reqVO.getAttachments());
|
||||
|
||||
TaskWorklogDO update = new TaskWorklogDO();
|
||||
update.setId(worklog.getId());
|
||||
update.setStartDate(reqVO.getStartDate());
|
||||
update.setEndDate(reqVO.getEndDate());
|
||||
update.setDurationHours(reqVO.getDurationHours());
|
||||
update.setProgressRate(reqVO.getProgressRate());
|
||||
update.setWorkContent(normalizeNullableText(reqVO.getWorkContent()));
|
||||
update.setAttachments(reqVO.getAttachments());
|
||||
update.setDifficulty(reqVO.getDifficulty());
|
||||
taskWorklogMapper.updateById(update);
|
||||
|
||||
PersonalItemDO current = cloneItem(item);
|
||||
syncOwnerProgressFromLatestWorklog(current);
|
||||
personalItemMapper.updateById(current);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteWorklog(Long id, Long worklogId) {
|
||||
PersonalItemDO item = validateItemExists(id);
|
||||
personalItemAccessService.checkCanWrite(item);
|
||||
validateItemAllowEdit(item);
|
||||
|
||||
TaskWorklogDO worklog = loadWorklog(worklogId, item.getId());
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
boolean isFiler = Objects.equals(worklog.getUserId(), loginUserId);
|
||||
boolean isOwner = Objects.equals(item.getOwnerId(), loginUserId);
|
||||
if (!isFiler && !isOwner) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_DELETE_FORBIDDEN);
|
||||
}
|
||||
taskWorklogMapper.deleteById(worklog.getId());
|
||||
|
||||
PersonalItemDO current = cloneItem(item);
|
||||
syncOwnerProgressFromLatestWorklog(current);
|
||||
personalItemMapper.updateById(current);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteWorklogs(Long id, List<Long> ids) {
|
||||
if (CollectionUtils.isEmpty(ids)) {
|
||||
throw invalidParamException("涓汉浜嬮」宸ヤ綔鏃ュ織缂栧彿鍒楄〃涓嶈兘涓虹┖");
|
||||
}
|
||||
for (Long worklogId : ids) {
|
||||
deleteWorklog(id, worklogId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteItem(Long id) {
|
||||
PersonalItemDO item = validateItemExists(id);
|
||||
personalItemAccessService.checkCanWrite(item);
|
||||
String fromStatus = item.getStatusCode();
|
||||
ObjectStatusModelDO statusModel = objectStatusModelMapper
|
||||
.selectByObjectTypeAndStatusCodeEnabled(PersonalItemConstants.STATUS_OBJECT_TYPE, fromStatus);
|
||||
if (statusModel == null || !Boolean.TRUE.equals(statusModel.getInitialFlag())) {
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_NOT_ALLOW_DELETE);
|
||||
}
|
||||
int deleteCount = personalItemMapper.deleteByIdAndStatus(id, fromStatus);
|
||||
if (deleteCount != 1) {
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_CONCURRENT_MODIFIED);
|
||||
}
|
||||
writeAuditLog(item, ObjectActivityConstants.PERSONAL_ITEM_ACTION_DELETE, fromStatus, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteItems(List<Long> ids) {
|
||||
if (CollectionUtils.isEmpty(ids)) {
|
||||
throw invalidParamException("个人事项编号列表不能为空");
|
||||
}
|
||||
for (Long id : ids) {
|
||||
deleteItem(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void relateExecution(List<Long> itemIds, Long executionId) {
|
||||
ProjectExecutionDO execution = projectExecutionMapper.selectById(executionId);
|
||||
if (execution == null) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_EXECUTION_NOT_EXISTS);
|
||||
}
|
||||
Long projectId = execution.getProjectId();
|
||||
for (Long itemId : itemIds) {
|
||||
PersonalItemDO item = validateItemExists(itemId);
|
||||
personalItemAccessService.checkCanWrite(item);
|
||||
personalItemAccessService.validateOwnerInExecution(executionId, item.getOwnerId());
|
||||
|
||||
ProjectTaskDO task = buildProjectTaskFromItem(item, projectId, executionId);
|
||||
projectTaskMapper.insert(task);
|
||||
personalItemMapper.deleteById(item.getId());
|
||||
writeAuditLog(item, ObjectActivityConstants.PERSONAL_ITEM_ACTION_DELETE,
|
||||
item.getStatusCode(), null, null, "关联执行后转为项目任务");
|
||||
}
|
||||
}
|
||||
|
||||
private ProjectTaskDO buildProjectTaskFromItem(PersonalItemDO item, Long projectId, Long executionId) {
|
||||
ProjectTaskDO task = new ProjectTaskDO();
|
||||
task.setId(item.getId());
|
||||
task.setProjectId(projectId);
|
||||
task.setExecutionId(executionId);
|
||||
task.setParentTaskId(null);
|
||||
task.setTaskTitle(item.getTaskTitle());
|
||||
task.setOwnerId(item.getOwnerId());
|
||||
task.setStatusCode(item.getStatusCode());
|
||||
task.setProgressRate(item.getProgressRate());
|
||||
task.setPlannedStartDate(item.getPlannedStartDate());
|
||||
task.setPlannedEndDate(item.getPlannedEndDate());
|
||||
task.setActualStartDate(item.getActualStartDate());
|
||||
task.setActualEndDate(item.getActualEndDate());
|
||||
task.setTaskDesc(item.getTaskDesc());
|
||||
task.setLastStatusReason(item.getLastStatusReason());
|
||||
task.setAttachments(item.getAttachments());
|
||||
return task;
|
||||
}
|
||||
|
||||
private PersonalItemDO validateItemExists(Long id) {
|
||||
PersonalItemDO item = personalItemMapper.selectById(id);
|
||||
if (item == null) {
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_NOT_EXISTS);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private PersonalItemDO validateOwnedItemExists(Long id) {
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
PersonalItemDO item = personalItemMapper.selectByIdAndOwnerId(id, loginUserId);
|
||||
if (item == null) {
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_NOT_EXISTS);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private void validateItemAllowEdit(PersonalItemDO item) {
|
||||
ObjectStatusModelDO statusModel = objectStatusModelMapper
|
||||
.selectByObjectTypeAndStatusCodeEnabled(PersonalItemConstants.STATUS_OBJECT_TYPE, item.getStatusCode());
|
||||
if (statusModel == null) {
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
||||
}
|
||||
if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_NOT_ALLOW_EDIT);
|
||||
}
|
||||
}
|
||||
|
||||
private String getInitialStatusCode() {
|
||||
ObjectStatusModelDO statusModel = objectStatusModelMapper
|
||||
.selectInitialByObjectTypeEnabled(PersonalItemConstants.STATUS_OBJECT_TYPE);
|
||||
if (statusModel == null || !StringUtils.hasText(statusModel.getStatusCode())) {
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
||||
}
|
||||
return statusModel.getStatusCode();
|
||||
}
|
||||
|
||||
private PersonalItemRespVO toRespVO(PersonalItemDO item) {
|
||||
return BeanUtils.toBean(item, PersonalItemRespVO.class, this::applyLifecycle);
|
||||
}
|
||||
|
||||
private void applyLifecycle(PersonalItemRespVO respVO) {
|
||||
PersonalItemStatusViewService.PersonalItemLifecycleView lifecycle =
|
||||
personalItemStatusViewService.getLifecycle(respVO.getStatusCode(), respVO.getProgressRate());
|
||||
respVO.setStatusName(lifecycle.statusName());
|
||||
respVO.setTerminal(lifecycle.terminal());
|
||||
respVO.setAllowEdit(lifecycle.allowEdit());
|
||||
respVO.setAvailableActions(lifecycle.availableActions());
|
||||
}
|
||||
|
||||
private void fillOwnerNicknames(List<PersonalItemRespVO> list) {
|
||||
if (CollectionUtils.isEmpty(list)) {
|
||||
return;
|
||||
}
|
||||
Collection<Long> ownerIds = list.stream()
|
||||
.map(PersonalItemRespVO::getOwnerId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
Map<Long, String> nicknameMap = loadUserNicknameMap(ownerIds);
|
||||
list.forEach(item -> item.setOwnerNickname(nicknameMap.get(item.getOwnerId())));
|
||||
}
|
||||
|
||||
private void fillTotalSpentHours(List<PersonalItemRespVO> list) {
|
||||
if (CollectionUtils.isEmpty(list)) {
|
||||
return;
|
||||
}
|
||||
Collection<Long> itemIds = list.stream()
|
||||
.map(PersonalItemRespVO::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
Map<Long, BigDecimal> spentHoursMap = loadTotalSpentHoursMap(itemIds);
|
||||
list.forEach(item -> item.setTotalSpentHours(
|
||||
spentHoursMap.getOrDefault(item.getId(), BigDecimal.ZERO)));
|
||||
}
|
||||
|
||||
private Map<Long, BigDecimal> loadTotalSpentHoursMap(Collection<Long> itemIds) {
|
||||
if (itemIds == null || itemIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<Map<String, Object>> rows = taskWorklogMapper.sumDurationGroupByTaskIds(itemIds);
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<Long, BigDecimal> result = new HashMap<>(rows.size());
|
||||
for (Map<String, Object> row : rows) {
|
||||
Long itemId = toLong(row.getOrDefault("taskId", row.get("task_id")));
|
||||
BigDecimal total = toBigDecimal(row.get("total"));
|
||||
if (itemId != null && total != null) {
|
||||
result.put(itemId, total);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Long toLong(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
if (value != null && StringUtils.hasText(value.toString())) {
|
||||
return Long.valueOf(value.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private BigDecimal toBigDecimal(Object value) {
|
||||
if (value instanceof BigDecimal bigDecimal) {
|
||||
return bigDecimal;
|
||||
}
|
||||
if (value != null && StringUtils.hasText(value.toString())) {
|
||||
return new BigDecimal(value.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private BigDecimal defaultDuration(BigDecimal value) {
|
||||
return value == null ? BigDecimal.ZERO : value;
|
||||
}
|
||||
|
||||
private String loadUserNickname(Long userId) {
|
||||
if (userId == null) {
|
||||
return null;
|
||||
}
|
||||
return loadUserNicknameMap(List.of(userId)).get(userId);
|
||||
}
|
||||
|
||||
private void applyLifecycleFields(PersonalItemDO item, String fromStatus, String toStatus) {
|
||||
ObjectStatusModelDO fromModel = objectStatusModelMapper
|
||||
.selectByObjectTypeAndStatusCodeEnabled(PersonalItemConstants.STATUS_OBJECT_TYPE, fromStatus);
|
||||
ObjectStatusModelDO toModel = objectStatusModelMapper
|
||||
.selectByObjectTypeAndStatusCodeEnabled(PersonalItemConstants.STATUS_OBJECT_TYPE, toStatus);
|
||||
if (fromModel == null || toModel == null) {
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
||||
}
|
||||
if (Boolean.TRUE.equals(fromModel.getInitialFlag()) && item.getActualStartDate() == null) {
|
||||
item.setActualStartDate(LocalDate.now());
|
||||
}
|
||||
if (Boolean.TRUE.equals(toModel.getTerminalFlag())) {
|
||||
item.setActualEndDate(LocalDate.now());
|
||||
if (PersonalItemConstants.STATUS_COMPLETED.equals(toStatus)) {
|
||||
item.setProgressRate(BigDecimal.valueOf(100).setScale(2, RoundingMode.HALF_UP));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Boolean.TRUE.equals(fromModel.getTerminalFlag()) && !Boolean.TRUE.equals(toModel.getTerminalFlag())) {
|
||||
item.setActualEndDate(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void writeAuditLog(PersonalItemDO item,
|
||||
String actionType,
|
||||
String fromStatus,
|
||||
String toStatus,
|
||||
String fieldChanges,
|
||||
String reason) {
|
||||
BizAuditLogDO auditLog = new BizAuditLogDO();
|
||||
auditLog.setBizType(PersonalItemConstants.BIZ_TYPE);
|
||||
auditLog.setBizId(item.getId());
|
||||
auditLog.setActionType(actionType);
|
||||
auditLog.setFromStatus(fromStatus);
|
||||
auditLog.setToStatus(toStatus);
|
||||
auditLog.setFieldChanges(fieldChanges);
|
||||
auditLog.setReason(reason);
|
||||
auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
|
||||
auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||
bizAuditLogMapper.insert(auditLog);
|
||||
}
|
||||
|
||||
private PersonalItemDO cloneItem(PersonalItemDO source) {
|
||||
PersonalItemDO target = new PersonalItemDO();
|
||||
target.setId(source.getId());
|
||||
target.setTaskTitle(source.getTaskTitle());
|
||||
target.setOwnerId(source.getOwnerId());
|
||||
target.setStatusCode(source.getStatusCode());
|
||||
target.setProgressRate(source.getProgressRate());
|
||||
target.setPlannedStartDate(source.getPlannedStartDate());
|
||||
target.setPlannedEndDate(source.getPlannedEndDate());
|
||||
target.setActualStartDate(source.getActualStartDate());
|
||||
target.setActualEndDate(source.getActualEndDate());
|
||||
target.setTaskDesc(source.getTaskDesc());
|
||||
target.setLastStatusReason(source.getLastStatusReason());
|
||||
target.setAttachments(source.getAttachments());
|
||||
return target;
|
||||
}
|
||||
|
||||
private String buildFieldChanges(PersonalItemDO before, PersonalItemDO after) {
|
||||
Map<String, Object> fieldChanges = new LinkedHashMap<>();
|
||||
appendFieldChange(fieldChanges, "taskTitle", valueOf(before, PersonalItemDO::getTaskTitle),
|
||||
valueOf(after, PersonalItemDO::getTaskTitle));
|
||||
appendFieldChange(fieldChanges, "ownerId", valueOf(before, PersonalItemDO::getOwnerId),
|
||||
valueOf(after, PersonalItemDO::getOwnerId));
|
||||
appendFieldChange(fieldChanges, "statusCode", valueOf(before, PersonalItemDO::getStatusCode),
|
||||
valueOf(after, PersonalItemDO::getStatusCode));
|
||||
appendFieldChange(fieldChanges, "progressRate", valueOf(before, PersonalItemDO::getProgressRate),
|
||||
valueOf(after, PersonalItemDO::getProgressRate));
|
||||
appendFieldChange(fieldChanges, "plannedStartDate", valueOf(before, PersonalItemDO::getPlannedStartDate),
|
||||
valueOf(after, PersonalItemDO::getPlannedStartDate));
|
||||
appendFieldChange(fieldChanges, "plannedEndDate", valueOf(before, PersonalItemDO::getPlannedEndDate),
|
||||
valueOf(after, PersonalItemDO::getPlannedEndDate));
|
||||
appendFieldChange(fieldChanges, "actualStartDate", valueOf(before, PersonalItemDO::getActualStartDate),
|
||||
valueOf(after, PersonalItemDO::getActualStartDate));
|
||||
appendFieldChange(fieldChanges, "actualEndDate", valueOf(before, PersonalItemDO::getActualEndDate),
|
||||
valueOf(after, PersonalItemDO::getActualEndDate));
|
||||
appendFieldChange(fieldChanges, "taskDesc", valueOf(before, PersonalItemDO::getTaskDesc),
|
||||
valueOf(after, PersonalItemDO::getTaskDesc));
|
||||
appendFieldChange(fieldChanges, "lastStatusReason", valueOf(before, PersonalItemDO::getLastStatusReason),
|
||||
valueOf(after, PersonalItemDO::getLastStatusReason));
|
||||
appendFieldChange(fieldChanges, "attachments", valueOf(before, PersonalItemDO::getAttachments),
|
||||
valueOf(after, PersonalItemDO::getAttachments));
|
||||
return fieldChanges.isEmpty() ? null : JsonUtils.toJsonString(fieldChanges);
|
||||
}
|
||||
|
||||
private <T> T valueOf(PersonalItemDO item, Function<PersonalItemDO, T> getter) {
|
||||
return item == null ? null : getter.apply(item);
|
||||
}
|
||||
|
||||
private void appendFieldChange(Map<String, Object> fieldChanges, String fieldName, Object before, Object after) {
|
||||
if (Objects.equals(before, after)) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> value = new LinkedHashMap<>();
|
||||
value.put("before", before);
|
||||
value.put("after", after);
|
||||
fieldChanges.put(fieldName, value);
|
||||
}
|
||||
|
||||
private void validateDateRange(LocalDate startDate, LocalDate endDate, String message) {
|
||||
if (startDate == null || endDate == null) {
|
||||
return;
|
||||
}
|
||||
if (endDate.isBefore(startDate)) {
|
||||
throw invalidParamException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal normalizeProgress(BigDecimal value) {
|
||||
BigDecimal progress = value == null ? BigDecimal.ZERO : value;
|
||||
return progress.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private void validateDurationGranularity(BigDecimal durationHours) {
|
||||
if (durationHours == null
|
||||
|| durationHours.compareTo(BigDecimal.ZERO) <= 0
|
||||
|| durationHours.remainder(DURATION_GRANULARITY_HOURS).compareTo(BigDecimal.ZERO) != 0) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_DURATION_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateNoOverlap(Long taskId, Long userId, LocalDate startDate, LocalDate endDate, Long excludeId) {
|
||||
if (taskWorklogMapper.existsOverlapping(taskId, userId, startDate, endDate, excludeId)) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_DATE_OVERLAP);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateProgressMonotonicity(Long taskId, Long userId, LocalDate endDate,
|
||||
BigDecimal progressRate, Long excludeId) {
|
||||
if (progressRate == null || endDate == null) {
|
||||
return;
|
||||
}
|
||||
TaskWorklogDO prev = taskWorklogMapper.selectPrevByEndDate(taskId, userId, endDate, excludeId);
|
||||
if (prev != null && prev.getProgressRate() != null
|
||||
&& prev.getProgressRate().compareTo(progressRate) > 0) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_PROGRESS_NOT_MONOTONIC);
|
||||
}
|
||||
TaskWorklogDO next = taskWorklogMapper.selectNextByEndDate(taskId, userId, endDate, excludeId);
|
||||
if (next != null && next.getProgressRate() != null
|
||||
&& next.getProgressRate().compareTo(progressRate) < 0) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_PROGRESS_NOT_MONOTONIC);
|
||||
}
|
||||
}
|
||||
|
||||
private TaskWorklogDO buildWorklog(Long taskId, Long userId, TaskWorklogSaveReqVO reqVO) {
|
||||
TaskWorklogDO worklog = new TaskWorklogDO();
|
||||
worklog.setTaskId(taskId);
|
||||
worklog.setUserId(userId);
|
||||
worklog.setStartDate(reqVO.getStartDate());
|
||||
worklog.setEndDate(reqVO.getEndDate());
|
||||
worklog.setDurationHours(reqVO.getDurationHours());
|
||||
worklog.setProgressRate(reqVO.getProgressRate());
|
||||
worklog.setWorkContent(normalizeNullableText(reqVO.getWorkContent()));
|
||||
worklog.setAttachments(reqVO.getAttachments());
|
||||
worklog.setDifficulty(reqVO.getDifficulty());
|
||||
return worklog;
|
||||
}
|
||||
|
||||
private TaskWorklogDO loadWorklog(Long worklogId, Long taskId) {
|
||||
if (worklogId == null) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_NOT_EXISTS);
|
||||
}
|
||||
TaskWorklogDO worklog = taskWorklogMapper.selectByIdAndTaskId(worklogId, taskId);
|
||||
if (worklog == null) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_NOT_EXISTS);
|
||||
}
|
||||
return worklog;
|
||||
}
|
||||
|
||||
private void maybeAutoStartByWorklog(PersonalItemDO item) {
|
||||
String fromStatus = item.getStatusCode();
|
||||
ObjectStatusModelDO fromModel = objectStatusModelMapper
|
||||
.selectByObjectTypeAndStatusCodeEnabled(PersonalItemConstants.STATUS_OBJECT_TYPE, fromStatus);
|
||||
if (fromModel == null || !Boolean.TRUE.equals(fromModel.getInitialFlag())) {
|
||||
return;
|
||||
}
|
||||
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
||||
.selectByObjectTypeAndFromStatusAndAction(PersonalItemConstants.STATUS_OBJECT_TYPE, fromStatus,
|
||||
WORKLOG_AUTO_START_ACTION);
|
||||
if (transition == null || !StringUtils.hasText(transition.getToStatusCode())) {
|
||||
return;
|
||||
}
|
||||
String toStatus = transition.getToStatusCode();
|
||||
int updateCount = personalItemMapper.updateStatusByIdAndStatus(item.getId(), fromStatus, toStatus,
|
||||
WORKLOG_AUTO_START_REASON);
|
||||
if (updateCount != 1) {
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_CONCURRENT_MODIFIED);
|
||||
}
|
||||
item.setStatusCode(toStatus);
|
||||
item.setLastStatusReason(WORKLOG_AUTO_START_REASON);
|
||||
applyLifecycleFields(item, fromStatus, toStatus);
|
||||
}
|
||||
|
||||
private void syncOwnerProgressFromLatestWorklog(PersonalItemDO item) {
|
||||
if (item == null || item.getId() == null || item.getOwnerId() == null) {
|
||||
return;
|
||||
}
|
||||
TaskWorklogDO latest = taskWorklogMapper.selectLatestByTaskIdAndUserId(item.getId(), item.getOwnerId());
|
||||
if (latest == null || latest.getProgressRate() == null) {
|
||||
return;
|
||||
}
|
||||
item.setProgressRate(normalizeProgress(latest.getProgressRate()));
|
||||
}
|
||||
|
||||
private Map<Long, String> loadUserNicknameMap(Collection<Long> userIds) {
|
||||
if (userIds == null || userIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
|
||||
if (userMap == null || userMap.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<Long, String> nicknameMap = new HashMap<>(userMap.size());
|
||||
userMap.forEach((userId, user) -> nicknameMap.put(userId, user == null ? null : user.getNickname()));
|
||||
return nicknameMap;
|
||||
}
|
||||
|
||||
private String normalizeRequiredTitle(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
throw invalidParamException("个人事项标题不能为空");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private String normalizeRequiredActionCode(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
throw invalidParamException("动作编码不能为空");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private String normalizeNullableText(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private String defaultText(String value) {
|
||||
return StringUtils.hasText(value) ? value : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.njcn.rdms.module.project.service.personal;
|
||||
|
||||
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
|
||||
import com.njcn.rdms.module.project.constant.PersonalItemConstants;
|
||||
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemLifecycleActionRespVO;
|
||||
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 jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
|
||||
@Service
|
||||
public class PersonalItemStatusViewService {
|
||||
|
||||
private static final String ACTION_COMPLETE = "complete";
|
||||
private static final BigDecimal COMPLETE_PROGRESS_THRESHOLD = BigDecimal.valueOf(100);
|
||||
|
||||
@Resource
|
||||
private ObjectStatusModelMapper objectStatusModelMapper;
|
||||
@Resource
|
||||
private ObjectStatusTransitionMapper objectStatusTransitionMapper;
|
||||
|
||||
public PersonalItemLifecycleView getLifecycle(String statusCode, BigDecimal progressRate) {
|
||||
ObjectStatusModelDO statusModel = objectStatusModelMapper
|
||||
.selectByObjectTypeAndStatusCodeEnabled(PersonalItemConstants.STATUS_OBJECT_TYPE, statusCode);
|
||||
if (statusModel == null) {
|
||||
throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
|
||||
}
|
||||
return new PersonalItemLifecycleView(
|
||||
statusModel.getStatusName(),
|
||||
statusModel.getTerminalFlag(),
|
||||
statusModel.getAllowEdit(),
|
||||
buildAvailableActions(statusCode, progressRate)
|
||||
);
|
||||
}
|
||||
|
||||
public List<PersonalItemLifecycleActionRespVO> buildAvailableActions(String statusCode, BigDecimal progressRate) {
|
||||
List<ObjectStatusTransitionDO> transitions = objectStatusTransitionMapper
|
||||
.selectListByObjectTypeAndFromStatus(PersonalItemConstants.STATUS_OBJECT_TYPE, statusCode);
|
||||
if (transitions == null || transitions.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return transitions.stream()
|
||||
.filter(transition -> !ObjectActivityConstants.TASK_ACTION_AUTO_START.equals(transition.getActionCode()))
|
||||
.filter(transition -> !ACTION_COMPLETE.equals(transition.getActionCode())
|
||||
|| isCompleteProgressSatisfied(progressRate))
|
||||
.map(transition -> {
|
||||
PersonalItemLifecycleActionRespVO action = new PersonalItemLifecycleActionRespVO();
|
||||
action.setActionCode(transition.getActionCode());
|
||||
action.setActionName(transition.getActionName());
|
||||
action.setNeedReason(transition.getNeedReason());
|
||||
return action;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private boolean isCompleteProgressSatisfied(BigDecimal progressRate) {
|
||||
return progressRate != null && progressRate.compareTo(COMPLETE_PROGRESS_THRESHOLD) >= 0;
|
||||
}
|
||||
|
||||
public record PersonalItemLifecycleView(String statusName,
|
||||
Boolean terminal,
|
||||
Boolean allowEdit,
|
||||
List<PersonalItemLifecycleActionRespVO> availableActions) {
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execut
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 执行主数据 Service。
|
||||
*/
|
||||
@@ -33,6 +35,8 @@ public interface ProjectExecutionService {
|
||||
*/
|
||||
PageResult<ProjectExecutionRespVO> getExecutionRespVOPage(Long projectId, ProjectExecutionPageReqVO reqVO);
|
||||
|
||||
List<ProjectExecutionRespVO> getCurrentUserExecutionList();
|
||||
|
||||
void changeOwner(Long projectId, Long executionId, ProjectExecutionOwnerChangeReqVO reqVO);
|
||||
|
||||
/**
|
||||
|
||||
@@ -248,6 +248,40 @@ public class ProjectExecutionServiceImpl implements ProjectExecutionService {
|
||||
return voPageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProjectExecutionRespVO> getCurrentUserExecutionList() {
|
||||
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
|
||||
List<ProjectExecutionDO> executions = projectExecutionMapper.selectListByOwnerId(loginUserId);
|
||||
if (executions == null || executions.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<ProjectExecutionRespVO> list = BeanUtils.toBean(executions, ProjectExecutionRespVO.class);
|
||||
Map<Long, Boolean> rootTasksAllCompletedMap = new HashMap<>();
|
||||
list.stream()
|
||||
.filter(vo -> vo.getProjectId() != null)
|
||||
.collect(Collectors.groupingBy(ProjectExecutionRespVO::getProjectId, LinkedHashMap::new,
|
||||
Collectors.toList()))
|
||||
.forEach((groupProjectId, projectList) -> {
|
||||
fillExecutionProgress(groupProjectId, projectList);
|
||||
rootTasksAllCompletedMap.putAll(loadExecutionRootTasksAllCompletedMap(groupProjectId, projectList));
|
||||
});
|
||||
Set<Long> ownerIds = list.stream()
|
||||
.map(ProjectExecutionRespVO::getOwnerId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
Map<Long, String> nicknameMap = loadOwnerNicknameMap(ownerIds);
|
||||
list.forEach(vo -> {
|
||||
vo.setOwnerNickname(nicknameMap.get(vo.getOwnerId()));
|
||||
try {
|
||||
applyLifecycle(vo, rootTasksAllCompletedMap.getOrDefault(vo.getId(), false));
|
||||
} catch (Exception e) {
|
||||
log.warn("execution lifecycle apply failed in current user list assembly. executionId={}, statusCode={}, error={}",
|
||||
vo.getId(), vo.getStatusCode(), e.getMessage());
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||
|
||||
@@ -36,13 +36,7 @@ import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
@@ -218,6 +212,7 @@ public class TaskWorklogServiceImpl implements TaskWorklogService {
|
||||
worklog.setProgressRate(reqVO.getProgressRate());
|
||||
worklog.setWorkContent(normalizeNullableText(reqVO.getWorkContent()));
|
||||
worklog.setAttachments(reqVO.getAttachments());
|
||||
worklog.setDifficulty(reqVO.getDifficulty());
|
||||
return worklog;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.njcn.rdms.module.project.service.project.execution;
|
||||
|
||||
import com.njcn.rdms.framework.common.exception.ServiceException;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
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.project.execution.vo.execution.ProjectExecutionOwnerChangeReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.execution.ProjectExecutionPageReqVO;
|
||||
@@ -31,11 +32,13 @@ import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
|
||||
import com.njcn.rdms.module.system.api.dict.DictDataApi;
|
||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -56,6 +59,7 @@ import static org.mockito.ArgumentMatchers.anyCollection;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -538,6 +542,43 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
||||
assertEquals(new BigDecimal("10.00"), result.getList().get(1).getProgressRate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCurrentUserExecutionList_shouldQueryByLoginUserAndAssembleAcrossProjects() {
|
||||
Long loginUserId = 3001L;
|
||||
ProjectExecutionDO first = createExecution(2001L, 5001L, loginUserId);
|
||||
ProjectExecutionDO second = createExecution(2002L, 5002L, loginUserId);
|
||||
|
||||
when(projectExecutionMapper.selectListByOwnerId(loginUserId)).thenReturn(List.of(first, second));
|
||||
when(objectStatusModelMapper.selectProgressExcludedStatusCodesByObjectTypeEnabled("task"))
|
||||
.thenReturn(List.of("cancelled"));
|
||||
when(projectTaskMapper.selectRootTaskAvgProgressGroupByExecutionIds(eq(2001L), anyCollection(), eq(List.of("cancelled"))))
|
||||
.thenReturn(List.of(Map.of("execution_id", 5001L, "progress_rate", new BigDecimal("25.555"))));
|
||||
when(projectTaskMapper.selectRootTaskAvgProgressGroupByExecutionIds(eq(2002L), anyCollection(), eq(List.of("cancelled"))))
|
||||
.thenReturn(List.of(Map.of("execution_id", 5002L, "progress_rate", new BigDecimal("66.666"))));
|
||||
when(projectExecutionStatusViewService.getLifecycle(eq("pending"), eq(loginUserId), eq(new BigDecimal("25.56")), anyBoolean()))
|
||||
.thenReturn(createLifecycleView());
|
||||
when(projectExecutionStatusViewService.getLifecycle(eq("pending"), eq(loginUserId), eq(new BigDecimal("66.67")), anyBoolean()))
|
||||
.thenReturn(createLifecycleView());
|
||||
when(adminUserApi.getUserMap(anyCollection())).thenReturn(Map.of(loginUserId, createUser(loginUserId, "测试负责人")));
|
||||
|
||||
try (MockedStatic<SecurityFrameworkUtils> mocked = mockStatic(SecurityFrameworkUtils.class)) {
|
||||
mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(loginUserId);
|
||||
|
||||
List<ProjectExecutionRespVO> result = projectExecutionService.getCurrentUserExecutionList();
|
||||
|
||||
assertEquals(2, result.size());
|
||||
assertEquals(new BigDecimal("25.56"), result.get(0).getProgressRate());
|
||||
assertEquals(new BigDecimal("66.67"), result.get(1).getProgressRate());
|
||||
assertEquals("测试负责人", result.get(0).getOwnerNickname());
|
||||
assertEquals("测试负责人", result.get(1).getOwnerNickname());
|
||||
verify(projectExecutionMapper).selectListByOwnerId(loginUserId);
|
||||
verify(projectTaskMapper).selectRootTaskAvgProgressGroupByExecutionIds(eq(2001L), anyCollection(),
|
||||
eq(List.of("cancelled")));
|
||||
verify(projectTaskMapper).selectRootTaskAvgProgressGroupByExecutionIds(eq(2002L), anyCollection(),
|
||||
eq(List.of("cancelled")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getExecutionPage_shouldDelegateMapper() {
|
||||
Long projectId = 2001L;
|
||||
@@ -616,4 +657,11 @@ class ProjectExecutionServiceImplTest extends BaseMockitoUnitTest {
|
||||
execution.setDeleted(Boolean.FALSE);
|
||||
return execution;
|
||||
}
|
||||
|
||||
private AdminUserRespDTO createUser(Long userId, String nickname) {
|
||||
AdminUserRespDTO user = new AdminUserRespDTO();
|
||||
user.setId(userId);
|
||||
user.setNickname(nickname);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user