feat(新增加班申请功能): 新增申请功能,可在工作台进行审核。

fix(dict_data): 字典数据的颜色类型字段不允许null更新。
This commit is contained in:
dk
2026-06-01 21:25:02 +08:00
parent 5c7dbf7286
commit e71140d8a2
24 changed files with 1247 additions and 1 deletions

View File

@@ -227,4 +227,17 @@ public interface ErrorCodeConstants {
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, "无权修改个人事项");
// ========== 加班申请 1_008_009_xxx ==========
ErrorCode OVERTIME_APPLICATION_NOT_EXISTS = new ErrorCode(1_008_009_001, "加班申请不存在");
ErrorCode OVERTIME_APPLICATION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED = new ErrorCode(1_008_009_002, "加班申请状态定义不存在或已停用");
ErrorCode OVERTIME_APPLICATION_STATUS_ACTION_NOT_ALLOWED = new ErrorCode(1_008_009_003, "当前加班申请状态不支持动作【{}】");
ErrorCode OVERTIME_APPLICATION_STATUS_ACTION_REASON_REQUIRED = new ErrorCode(1_008_009_004, "动作【{}】必须填写原因");
ErrorCode OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED = new ErrorCode(1_008_009_005, "加班申请状态已发生变化,请刷新后重试");
ErrorCode OVERTIME_APPLICATION_APPLICANT_ONLY = new ErrorCode(1_008_009_006, "仅申请人可执行该操作");
ErrorCode OVERTIME_APPLICATION_APPROVER_ONLY = new ErrorCode(1_008_009_007, "仅当前审核人可执行该操作");
ErrorCode OVERTIME_APPLICATION_APPROVER_INVALID = new ErrorCode(1_008_009_008, "审核人不是有效系统用户");
ErrorCode OVERTIME_APPLICATION_APPROVER_SELF_FORBIDDEN = new ErrorCode(1_008_009_009, "审核人不能选择申请人本人");
ErrorCode OVERTIME_APPLICATION_READ_FORBIDDEN = new ErrorCode(1_008_009_010, "无权查看该加班申请");
ErrorCode OVERTIME_APPLICATION_DELETE_ONLY_CANCELLED = new ErrorCode(1_008_009_011, "仅已撤销的加班申请允许删除");
}

View File

@@ -0,0 +1,33 @@
package com.njcn.rdms.module.project.constant;
/**
* 加班申请常量。
*/
public final class OvertimeApplicationConstants {
private OvertimeApplicationConstants() {
}
public static final String BIZ_TYPE = "overtime_application";
public static final String STATUS_OBJECT_TYPE = BIZ_TYPE;
public static final String STATUS_PENDING = "pending";
public static final String STATUS_APPROVED = "approved";
public static final String STATUS_REJECTED = "rejected";
public static final String STATUS_CANCELLED = "cancelled";
public static final String ACTION_SUBMIT = "submit";
public static final String ACTION_RESUBMIT = "resubmit";
public static final String ACTION_APPROVE = "approve";
public static final String ACTION_REJECT = "reject";
public static final String ACTION_CANCEL = "cancel";
public static final String ACTION_DELETE = "delete";
public static final String PERMISSION_QUERY = "project:overtime-application:query";
public static final String PERMISSION_CREATE = "project:overtime-application:create";
public static final String PERMISSION_UPDATE = "project:overtime-application:update";
public static final String PERMISSION_DELETE = "project:overtime-application:delete";
public static final String PERMISSION_APPROVE = "project:overtime-application:approve";
public static final String PERMISSION_EXPORT = "project:overtime-application:export";
}

View File

@@ -0,0 +1,135 @@
package com.njcn.rdms.module.project.controller.admin.overtime;
import com.njcn.rdms.framework.apilog.core.annotation.ApiAccessLog;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.excel.core.util.ExcelUtils;
import com.njcn.rdms.module.project.constant.OvertimeApplicationConstants;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationExportVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusActionReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusLogRespVO;
import com.njcn.rdms.module.project.service.overtime.OvertimeApplicationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.time.LocalDate;
import java.util.List;
import static com.njcn.rdms.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 加班申请")
@RestController
@RequestMapping("/project/overtime-applications")
@Validated
public class OvertimeApplicationController {
@Resource
private OvertimeApplicationService overtimeApplicationService;
@PostMapping
@Operation(summary = "新增加班申请并提交")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_CREATE + "')")
public CommonResult<Long> create(@Valid @RequestBody OvertimeApplicationSaveReqVO reqVO) {
return success(overtimeApplicationService.createApplication(reqVO));
}
@PutMapping("/{id}")
@Operation(summary = "退回后修改并重新提交加班申请")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_UPDATE + "')")
public CommonResult<Boolean> updateRejected(@PathVariable("id") Long id,
@Valid @RequestBody OvertimeApplicationSaveReqVO reqVO) {
overtimeApplicationService.updateRejectedApplication(id, reqVO);
return success(true);
}
@GetMapping("/{id}")
@Operation(summary = "获取加班申请详情")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_QUERY + "')")
public CommonResult<OvertimeApplicationRespVO> get(@PathVariable("id") Long id) {
return success(overtimeApplicationService.getApplication(id));
}
@GetMapping("/page")
@Operation(summary = "获取我的加班申请分页")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_QUERY + "')")
public CommonResult<PageResult<OvertimeApplicationRespVO>> page(@Valid OvertimeApplicationPageReqVO reqVO) {
return success(overtimeApplicationService.getMyPage(reqVO));
}
@GetMapping("/approval-page")
@Operation(summary = "获取待我审批的加班申请分页")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_APPROVE + "')")
public CommonResult<PageResult<OvertimeApplicationRespVO>> approvalPage(@Valid OvertimeApplicationPageReqVO reqVO) {
return success(overtimeApplicationService.getApprovalPage(reqVO));
}
@PostMapping("/{id}/approve")
@Operation(summary = "审核通过加班申请")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_APPROVE + "')")
public CommonResult<Boolean> approve(@PathVariable("id") Long id,
@Valid @RequestBody OvertimeApplicationStatusActionReqVO reqVO) {
overtimeApplicationService.approve(id, reqVO);
return success(true);
}
@PostMapping("/{id}/reject")
@Operation(summary = "审核退回加班申请")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_APPROVE + "')")
public CommonResult<Boolean> reject(@PathVariable("id") Long id,
@Valid @RequestBody OvertimeApplicationStatusActionReqVO reqVO) {
overtimeApplicationService.reject(id, reqVO);
return success(true);
}
@PostMapping("/{id}/cancel")
@Operation(summary = "撤销加班申请")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_UPDATE + "')")
public CommonResult<Boolean> cancel(@PathVariable("id") Long id,
@Valid @RequestBody OvertimeApplicationStatusActionReqVO reqVO) {
overtimeApplicationService.cancel(id, reqVO);
return success(true);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除已撤销的加班申请")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_DELETE + "')")
public CommonResult<Boolean> delete(@PathVariable("id") Long id) {
overtimeApplicationService.deleteApplication(id);
return success(true);
}
@GetMapping("/{id}/status-logs")
@Operation(summary = "获取加班申请状态日志")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_QUERY + "')")
public CommonResult<List<OvertimeApplicationStatusLogRespVO>> statusLogs(@PathVariable("id") Long id) {
return success(overtimeApplicationService.getStatusLogs(id));
}
@GetMapping("/export")
@Operation(summary = "导出我的加班申请")
@PreAuthorize("@ss.hasPermission('" + OvertimeApplicationConstants.PERMISSION_EXPORT + "')")
@ApiAccessLog(operateType = EXPORT)
public void export(@Valid OvertimeApplicationPageReqVO reqVO, HttpServletResponse response) throws IOException {
List<OvertimeApplicationExportVO> list = overtimeApplicationService.getExportList(reqVO);
ExcelUtils.write(response, "加班申请_" + LocalDate.now() + ".xls", "加班申请",
OvertimeApplicationExportVO.class, list);
}
}

View File

@@ -0,0 +1,40 @@
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@ExcelIgnoreUnannotated
public class OvertimeApplicationExportVO {
@ExcelProperty("申请人")
private String applicantName;
@ExcelProperty("加班日期")
private LocalDate overtimeDate;
@ExcelProperty("加班时长")
private String overtimeDuration;
@ExcelProperty("加班原因")
private String overtimeReason;
@ExcelProperty("加班内容")
private String overtimeContent;
@ExcelProperty("状态")
private String statusName;
@ExcelProperty("审核人")
private String approverName;
@ExcelProperty("提交时间")
private LocalDateTime submitTime;
@ExcelProperty("审核时间")
private LocalDateTime approvalTime;
}

View File

@@ -0,0 +1,44 @@
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
import com.njcn.rdms.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 加班申请分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class OvertimeApplicationPageReqVO extends PageParam {
@Schema(description = "关键词,匹配加班原因或加班内容", example = "上线")
private String keyword;
@Schema(description = "申请人姓名,模糊匹配", example = "张三")
private String applicantName;
@Schema(description = "审核人用户编号", example = "1001")
private Long approverId;
@Schema(description = "审核人姓名,模糊匹配", example = "李四")
private String approverName;
@Schema(description = "状态编码", example = "pending")
@Size(max = 32, message = "状态编码长度不能超过32个字符")
private String statusCode;
@Schema(description = "加班日期范围", example = "[2026-06-01, 2026-06-30]")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
private LocalDate[] overtimeDate;
@Schema(description = "创建时间", example = "[2026-06-01 00:00:00, 2026-06-30 23:59:59]")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@@ -0,0 +1,66 @@
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 加班申请 Response VO")
@Data
public class OvertimeApplicationRespVO {
@Schema(description = "加班申请编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9001")
private Long id;
@Schema(description = "申请人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3001")
private Long applicantId;
@Schema(description = "申请人姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
private String applicantName;
@Schema(description = "加班日期", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDate overtimeDate;
@Schema(description = "加班时长", requiredMode = Schema.RequiredMode.REQUIRED, example = "1天")
private String overtimeDuration;
@Schema(description = "加班原因", requiredMode = Schema.RequiredMode.REQUIRED)
private String overtimeReason;
@Schema(description = "加班内容", requiredMode = Schema.RequiredMode.REQUIRED)
private String overtimeContent;
@Schema(description = "审核人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
private Long approverId;
@Schema(description = "审核人姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
private String approverName;
@Schema(description = "状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "pending")
private String statusCode;
@Schema(description = "状态名称", example = "待审批")
private String statusName;
@Schema(description = "当前状态是否允许编辑", example = "false")
private Boolean allowEdit;
@Schema(description = "是否终态", example = "false")
private Boolean terminal;
@Schema(description = "最近一次审核意见")
private String approvalComment;
@Schema(description = "提交时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime submitTime;
@Schema(description = "最近一次审核时间")
private LocalDateTime approvalTime;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,37 @@
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDate;
@Schema(description = "管理后台 - 加班申请保存 Request VO")
@Data
public class OvertimeApplicationSaveReqVO {
@Schema(description = "加班日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-06-01")
@NotNull(message = "加班日期不能为空")
private LocalDate overtimeDate;
@Schema(description = "加班时长", requiredMode = Schema.RequiredMode.REQUIRED, example = "1天")
@NotBlank(message = "加班时长不能为空")
@Size(max = 30, message = "加班时长长度不能超过30个字符")
private String overtimeDuration;
@Schema(description = "加班原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "版本上线保障")
@NotBlank(message = "加班原因不能为空")
@Size(max = 500, message = "加班原因长度不能超过500个字符")
private String overtimeReason;
@Schema(description = "加班内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "处理上线验证和问题修复")
@NotBlank(message = "加班内容不能为空")
@Size(max = 1000, message = "加班内容长度不能超过1000个字符")
private String overtimeContent;
@Schema(description = "审核人用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
@NotNull(message = "审核人不能为空")
private Long approverId;
}

View File

@@ -0,0 +1,14 @@
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - 加班申请状态动作 Request VO")
@Data
public class OvertimeApplicationStatusActionReqVO {
@Schema(description = "动作原因或审核意见。退回、撤销必填", example = "请补充加班内容")
@Size(max = 1000, message = "动作原因长度不能超过1000个字符")
private String reason;
}

View File

@@ -0,0 +1,51 @@
package com.njcn.rdms.module.project.controller.admin.overtime.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 加班申请状态日志 Response VO")
@Data
public class OvertimeApplicationStatusLogRespVO {
@Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED)
private Long id;
@Schema(description = "加班申请编号", requiredMode = Schema.RequiredMode.REQUIRED)
private Long applicationId;
@Schema(description = "动作编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "approve")
private String actionType;
@Schema(description = "变更前状态", example = "pending")
private String fromStatus;
@Schema(description = "变更后状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "approved")
private String toStatus;
@Schema(description = "原因或审核意见")
private String reason;
@Schema(description = "操作人用户编号", requiredMode = Schema.RequiredMode.REQUIRED)
private Long operatorUserId;
@Schema(description = "操作人名称", requiredMode = Schema.RequiredMode.REQUIRED)
private String operatorName;
@Schema(description = "申请人姓名快照", requiredMode = Schema.RequiredMode.REQUIRED)
private String applicantNameSnapshot;
@Schema(description = "加班日期快照", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDate overtimeDateSnapshot;
@Schema(description = "加班时长快照", requiredMode = Schema.RequiredMode.REQUIRED)
private String overtimeDurationSnapshot;
@Schema(description = "备注")
private String remark;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,51 @@
package com.njcn.rdms.module.project.dal.dataobject.overtime;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 加班申请表。
*/
@TableName("rdms_overtime_application")
@Data
@EqualsAndHashCode(callSuper = true)
public class OvertimeApplicationDO extends BaseDO {
@TableId
private Long id;
private Long applicantId;
private String applicantName;
private LocalDate overtimeDate;
private String overtimeDuration;
private String overtimeReason;
private String overtimeContent;
private Long approverId;
private String approverName;
private String statusCode;
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private String approvalComment;
private LocalDateTime submitTime;
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private LocalDateTime approvalTime;
}

View File

@@ -0,0 +1,44 @@
package com.njcn.rdms.module.project.dal.dataobject.overtime;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
/**
* 加班申请状态日志表。
*/
@TableName("rdms_overtime_application_status_log")
@Data
@EqualsAndHashCode(callSuper = true)
public class OvertimeApplicationStatusLogDO extends BaseDO {
@TableId
private Long id;
private Long applicationId;
private String actionType;
private String fromStatus;
private String toStatus;
private String reason;
private Long operatorUserId;
private String operatorName;
private String applicantNameSnapshot;
private LocalDate overtimeDateSnapshot;
private String overtimeDurationSnapshot;
private String remark;
}

View File

@@ -0,0 +1,76 @@
package com.njcn.rdms.module.project.dal.mysql.overtime;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO;
import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationDO;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.util.StringUtils;
import java.util.Collection;
@Mapper
public interface OvertimeApplicationMapper extends BaseMapperX<OvertimeApplicationDO> {
default PageResult<OvertimeApplicationDO> selectMyPage(Long applicantId, OvertimeApplicationPageReqVO reqVO) {
LambdaQueryWrapperX<OvertimeApplicationDO> queryWrapper = buildPageQuery(reqVO);
queryWrapper.eq(OvertimeApplicationDO::getApplicantId, applicantId);
queryWrapper.orderByDesc(OvertimeApplicationDO::getSubmitTime)
.orderByDesc(OvertimeApplicationDO::getId);
return selectPage(reqVO, queryWrapper);
}
default PageResult<OvertimeApplicationDO> selectApprovalPage(Long approverId, OvertimeApplicationPageReqVO reqVO) {
LambdaQueryWrapperX<OvertimeApplicationDO> queryWrapper = buildPageQuery(reqVO);
queryWrapper.eq(OvertimeApplicationDO::getApproverId, approverId);
queryWrapper.orderByDesc(OvertimeApplicationDO::getSubmitTime)
.orderByDesc(OvertimeApplicationDO::getId);
return selectPage(reqVO, queryWrapper);
}
default OvertimeApplicationDO selectByIdAndApplicantId(Long id, Long applicantId) {
return selectOne(new LambdaQueryWrapperX<OvertimeApplicationDO>()
.eq(OvertimeApplicationDO::getId, id)
.eq(OvertimeApplicationDO::getApplicantId, applicantId));
}
default int updateByIdAndStatus(OvertimeApplicationDO update, Long id, String fromStatus) {
return update(update, new LambdaQueryWrapperX<OvertimeApplicationDO>()
.eq(OvertimeApplicationDO::getId, id)
.eq(OvertimeApplicationDO::getStatusCode, fromStatus));
}
default int updateByIdAndStatusAndApplicantId(OvertimeApplicationDO update, Long id, String fromStatus,
Long applicantId) {
return update(update, new LambdaQueryWrapperX<OvertimeApplicationDO>()
.eq(OvertimeApplicationDO::getId, id)
.eq(OvertimeApplicationDO::getStatusCode, fromStatus)
.eq(OvertimeApplicationDO::getApplicantId, applicantId));
}
default int updateByIdAndStatusesAndApplicantId(OvertimeApplicationDO update, Long id,
Collection<String> fromStatuses, Long applicantId) {
return update(update, new LambdaQueryWrapperX<OvertimeApplicationDO>()
.eq(OvertimeApplicationDO::getId, id)
.in(OvertimeApplicationDO::getStatusCode, fromStatuses)
.eq(OvertimeApplicationDO::getApplicantId, applicantId));
}
private LambdaQueryWrapperX<OvertimeApplicationDO> buildPageQuery(OvertimeApplicationPageReqVO reqVO) {
LambdaQueryWrapperX<OvertimeApplicationDO> queryWrapper = new LambdaQueryWrapperX<>();
queryWrapper.likeIfPresent(OvertimeApplicationDO::getApplicantName, reqVO.getApplicantName())
.eqIfPresent(OvertimeApplicationDO::getApproverId, reqVO.getApproverId())
.likeIfPresent(OvertimeApplicationDO::getApproverName, reqVO.getApproverName())
.eqIfPresent(OvertimeApplicationDO::getStatusCode, reqVO.getStatusCode())
.betweenIfPresent(OvertimeApplicationDO::getOvertimeDate, reqVO.getOvertimeDate())
.betweenIfPresent(BaseDO::getCreateTime, reqVO.getCreateTime());
if (StringUtils.hasText(reqVO.getKeyword())) {
queryWrapper.and(wrapper -> wrapper.like(OvertimeApplicationDO::getOvertimeReason, reqVO.getKeyword())
.or()
.like(OvertimeApplicationDO::getOvertimeContent, reqVO.getKeyword()));
}
return queryWrapper;
}
}

View File

@@ -0,0 +1,19 @@
package com.njcn.rdms.module.project.dal.mysql.overtime;
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationStatusLogDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface OvertimeApplicationStatusLogMapper extends BaseMapperX<OvertimeApplicationStatusLogDO> {
default List<OvertimeApplicationStatusLogDO> selectListByApplicationId(Long applicationId) {
return selectList(new LambdaQueryWrapperX<OvertimeApplicationStatusLogDO>()
.eq(OvertimeApplicationStatusLogDO::getApplicationId, applicationId)
.orderByDesc(OvertimeApplicationStatusLogDO::getCreateTime)
.orderByDesc(OvertimeApplicationStatusLogDO::getId));
}
}

View File

@@ -0,0 +1,36 @@
package com.njcn.rdms.module.project.service.overtime;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationExportVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusActionReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusLogRespVO;
import java.util.List;
public interface OvertimeApplicationService {
Long createApplication(OvertimeApplicationSaveReqVO reqVO);
void updateRejectedApplication(Long id, OvertimeApplicationSaveReqVO reqVO);
void approve(Long id, OvertimeApplicationStatusActionReqVO reqVO);
void reject(Long id, OvertimeApplicationStatusActionReqVO reqVO);
void cancel(Long id, OvertimeApplicationStatusActionReqVO reqVO);
void deleteApplication(Long id);
OvertimeApplicationRespVO getApplication(Long id);
PageResult<OvertimeApplicationRespVO> getMyPage(OvertimeApplicationPageReqVO reqVO);
PageResult<OvertimeApplicationRespVO> getApprovalPage(OvertimeApplicationPageReqVO reqVO);
List<OvertimeApplicationStatusLogRespVO> getStatusLogs(Long id);
List<OvertimeApplicationExportVO> getExportList(OvertimeApplicationPageReqVO reqVO);
}

View File

@@ -0,0 +1,467 @@
package com.njcn.rdms.module.project.service.overtime;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.pojo.PageParam;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.OvertimeApplicationConstants;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationExportVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusActionReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusLogRespVO;
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationDO;
import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationStatusLogDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
import com.njcn.rdms.module.project.dal.mysql.overtime.OvertimeApplicationMapper;
import com.njcn.rdms.module.project.dal.mysql.overtime.OvertimeApplicationStatusLogMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.system.api.user.AdminUserApi;
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
@Service
public class OvertimeApplicationServiceImpl implements OvertimeApplicationService {
@Resource
private OvertimeApplicationMapper overtimeApplicationMapper;
@Resource
private OvertimeApplicationStatusLogMapper overtimeApplicationStatusLogMapper;
@Resource
private BizAuditLogMapper bizAuditLogMapper;
@Resource
private ObjectStatusModelMapper objectStatusModelMapper;
@Resource
private ObjectStatusTransitionMapper objectStatusTransitionMapper;
@Resource
private AdminUserApi adminUserApi;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createApplication(OvertimeApplicationSaveReqVO reqVO) {
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
AdminUserRespDTO approver = validateApprover(reqVO.getApproverId());
String initialStatus = getInitialStatusCode();
OvertimeApplicationDO application = new OvertimeApplicationDO();
application.setApplicantId(loginUserId);
application.setApplicantName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
applySaveFields(application, reqVO, approver);
application.setStatusCode(initialStatus);
application.setApprovalComment(null);
application.setSubmitTime(LocalDateTime.now());
application.setApprovalTime(null);
overtimeApplicationMapper.insert(application);
writeStatusLog(application, OvertimeApplicationConstants.ACTION_SUBMIT, null, initialStatus, null);
writeAuditLog(application, OvertimeApplicationConstants.ACTION_SUBMIT, null, initialStatus, null, null, null);
return application.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateRejectedApplication(Long id, OvertimeApplicationSaveReqVO reqVO) {
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
OvertimeApplicationDO current = validateApplicationExists(id);
if (!Objects.equals(current.getApplicantId(), loginUserId)) {
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPLICANT_ONLY);
}
String fromStatus = current.getStatusCode();
ObjectStatusTransitionDO transition = validateTransition(fromStatus, OvertimeApplicationConstants.ACTION_RESUBMIT,
null);
AdminUserRespDTO approver = validateApprover(reqVO.getApproverId());
OvertimeApplicationDO before = cloneApplication(current);
OvertimeApplicationDO update = new OvertimeApplicationDO();
applySaveFields(update, reqVO, approver);
update.setStatusCode(transition.getToStatusCode());
update.setApprovalComment(null);
update.setSubmitTime(LocalDateTime.now());
update.setApprovalTime(null);
int updateCount = overtimeApplicationMapper.updateByIdAndStatusesAndApplicantId(update, id,
List.of(OvertimeApplicationConstants.STATUS_PENDING, OvertimeApplicationConstants.STATUS_REJECTED),
loginUserId);
if (updateCount != 1) {
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED);
}
OvertimeApplicationDO after = mergeUpdated(current, update);
writeStatusLog(after, OvertimeApplicationConstants.ACTION_RESUBMIT, fromStatus, transition.getToStatusCode(),
null);
writeAuditLog(after, OvertimeApplicationConstants.ACTION_RESUBMIT, fromStatus, transition.getToStatusCode(),
buildFieldChanges(before, after), null, null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void approve(Long id, OvertimeApplicationStatusActionReqVO reqVO) {
processApprovalAction(id, OvertimeApplicationConstants.ACTION_APPROVE, reqVO);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void reject(Long id, OvertimeApplicationStatusActionReqVO reqVO) {
processApprovalAction(id, OvertimeApplicationConstants.ACTION_REJECT, reqVO);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void cancel(Long id, OvertimeApplicationStatusActionReqVO reqVO) {
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
OvertimeApplicationDO current = validateApplicationExists(id);
if (!Objects.equals(current.getApplicantId(), loginUserId)) {
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPLICANT_ONLY);
}
String reason = normalizeNullableText(reqVO == null ? null : reqVO.getReason());
String fromStatus = current.getStatusCode();
ObjectStatusTransitionDO transition = validateTransition(fromStatus, OvertimeApplicationConstants.ACTION_CANCEL,
reason);
OvertimeApplicationDO update = new OvertimeApplicationDO();
update.setStatusCode(transition.getToStatusCode());
update.setApprovalComment(reason);
update.setApprovalTime(LocalDateTime.now());
int updateCount = overtimeApplicationMapper.updateByIdAndStatusAndApplicantId(update, id, fromStatus,
loginUserId);
if (updateCount != 1) {
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED);
}
OvertimeApplicationDO after = mergeUpdated(current, update);
writeStatusLog(after, OvertimeApplicationConstants.ACTION_CANCEL, fromStatus, transition.getToStatusCode(),
reason);
writeAuditLog(after, OvertimeApplicationConstants.ACTION_CANCEL, fromStatus, transition.getToStatusCode(),
null, reason, null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteApplication(Long id) {
OvertimeApplicationDO current = validateApplicationExists(id);
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
if (!Objects.equals(current.getApplicantId(), loginUserId)) {
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPLICANT_ONLY);
}
if (!OvertimeApplicationConstants.STATUS_CANCELLED.equals(current.getStatusCode())) {
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_DELETE_ONLY_CANCELLED);
}
overtimeApplicationMapper.deleteById(id);
writeAuditLog(current, OvertimeApplicationConstants.ACTION_DELETE, current.getStatusCode(), null, null, null,
null);
}
@Override
public OvertimeApplicationRespVO getApplication(Long id) {
OvertimeApplicationDO application = validateReadableApplication(id);
return toRespVO(application);
}
@Override
public PageResult<OvertimeApplicationRespVO> getMyPage(OvertimeApplicationPageReqVO reqVO) {
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
PageResult<OvertimeApplicationDO> page = overtimeApplicationMapper.selectMyPage(loginUserId, reqVO);
return BeanUtils.toBean(page, OvertimeApplicationRespVO.class, this::applyStatusView);
}
@Override
public PageResult<OvertimeApplicationRespVO> getApprovalPage(OvertimeApplicationPageReqVO reqVO) {
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
PageResult<OvertimeApplicationDO> page = overtimeApplicationMapper.selectApprovalPage(loginUserId, reqVO);
return BeanUtils.toBean(page, OvertimeApplicationRespVO.class, this::applyStatusView);
}
@Override
public List<OvertimeApplicationStatusLogRespVO> getStatusLogs(Long id) {
validateReadableApplication(id);
return BeanUtils.toBean(overtimeApplicationStatusLogMapper.selectListByApplicationId(id),
OvertimeApplicationStatusLogRespVO.class);
}
@Override
public List<OvertimeApplicationExportVO> getExportList(OvertimeApplicationPageReqVO reqVO) {
reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
PageResult<OvertimeApplicationRespVO> page = getMyPage(reqVO);
return BeanUtils.toBean(page.getList(), OvertimeApplicationExportVO.class);
}
private void processApprovalAction(Long id, String actionCode, OvertimeApplicationStatusActionReqVO reqVO) {
OvertimeApplicationDO current = validateApplicationExists(id);
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
if (!Objects.equals(current.getApproverId(), loginUserId)) {
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPROVER_ONLY);
}
String reason = normalizeNullableText(reqVO == null ? null : reqVO.getReason());
String fromStatus = current.getStatusCode();
ObjectStatusTransitionDO transition = validateTransition(fromStatus, actionCode, reason);
OvertimeApplicationDO update = new OvertimeApplicationDO();
update.setStatusCode(transition.getToStatusCode());
update.setApprovalComment(reason);
update.setApprovalTime(LocalDateTime.now());
int updateCount = overtimeApplicationMapper.updateByIdAndStatus(update, id, fromStatus);
if (updateCount != 1) {
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED);
}
OvertimeApplicationDO after = mergeUpdated(current, update);
writeStatusLog(after, actionCode, fromStatus, transition.getToStatusCode(), reason);
writeAuditLog(after, actionCode, fromStatus, transition.getToStatusCode(), null, reason, null);
}
private OvertimeApplicationDO validateApplicationExists(Long id) {
OvertimeApplicationDO application = overtimeApplicationMapper.selectById(id);
if (application == null) {
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_NOT_EXISTS);
}
return application;
}
private OvertimeApplicationDO validateReadableApplication(Long id) {
OvertimeApplicationDO application = validateApplicationExists(id);
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
if (!Objects.equals(application.getApplicantId(), loginUserId)
&& !Objects.equals(application.getApproverId(), loginUserId)) {
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_READ_FORBIDDEN);
}
return application;
}
private ObjectStatusTransitionDO validateTransition(String fromStatus, String actionCode, String reason) {
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
.selectByObjectTypeAndFromStatusAndAction(OvertimeApplicationConstants.STATUS_OBJECT_TYPE, fromStatus,
actionCode);
if (transition == null) {
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_ACTION_NOT_ALLOWED, actionCode);
}
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_ACTION_REASON_REQUIRED, actionCode);
}
ObjectStatusModelDO toModel = objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
OvertimeApplicationConstants.STATUS_OBJECT_TYPE, transition.getToStatusCode());
if (toModel == null) {
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
}
return transition;
}
private String getInitialStatusCode() {
ObjectStatusModelDO statusModel = objectStatusModelMapper
.selectInitialByObjectTypeEnabled(OvertimeApplicationConstants.STATUS_OBJECT_TYPE);
if (statusModel == null || !StringUtils.hasText(statusModel.getStatusCode())) {
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
}
return statusModel.getStatusCode();
}
private AdminUserRespDTO validateApprover(Long approverId) {
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
if (Objects.equals(approverId, loginUserId)) {
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPROVER_SELF_FORBIDDEN);
}
return loadUser(approverId);
}
private AdminUserRespDTO loadUser(Long userId) {
if (userId == null) {
throw invalidParamException("用户编号不能为空");
}
adminUserApi.validateUserList(Collections.singleton(userId)).getCheckedData();
CommonResult<AdminUserRespDTO> result = adminUserApi.getUser(userId);
AdminUserRespDTO user = result.getCheckedData();
if (user == null) {
throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPROVER_INVALID);
}
return user;
}
private void applySaveFields(OvertimeApplicationDO target, OvertimeApplicationSaveReqVO reqVO,
AdminUserRespDTO approver) {
target.setOvertimeDate(reqVO.getOvertimeDate());
target.setOvertimeDuration(normalizeRequiredText(reqVO.getOvertimeDuration(), "加班时长不能为空"));
target.setOvertimeReason(normalizeRequiredText(reqVO.getOvertimeReason(), "加班原因不能为空"));
target.setOvertimeContent(normalizeRequiredText(reqVO.getOvertimeContent(), "加班内容不能为空"));
target.setApproverId(approver.getId());
target.setApproverName(defaultText(approver.getNickname()));
}
private OvertimeApplicationRespVO toRespVO(OvertimeApplicationDO application) {
return BeanUtils.toBean(application, OvertimeApplicationRespVO.class, this::applyStatusView);
}
private void applyStatusView(OvertimeApplicationRespVO respVO) {
ObjectStatusModelDO statusModel = objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
OvertimeApplicationConstants.STATUS_OBJECT_TYPE, respVO.getStatusCode());
if (statusModel == null) {
respVO.setStatusName(respVO.getStatusCode());
respVO.setAllowEdit(false);
respVO.setTerminal(false);
return;
}
respVO.setStatusName(statusModel.getStatusName());
respVO.setAllowEdit(Boolean.TRUE.equals(statusModel.getAllowEdit()));
respVO.setTerminal(Boolean.TRUE.equals(statusModel.getTerminalFlag()));
}
private void writeStatusLog(OvertimeApplicationDO application, String actionType, String fromStatus,
String toStatus, String reason) {
OvertimeApplicationStatusLogDO log = new OvertimeApplicationStatusLogDO();
log.setApplicationId(application.getId());
log.setActionType(actionType);
log.setFromStatus(fromStatus);
log.setToStatus(toStatus);
log.setReason(reason);
log.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
log.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
log.setApplicantNameSnapshot(application.getApplicantName());
log.setOvertimeDateSnapshot(application.getOvertimeDate());
log.setOvertimeDurationSnapshot(application.getOvertimeDuration());
log.setRemark(buildSnapshotRemark(application));
overtimeApplicationStatusLogMapper.insert(log);
}
private void writeAuditLog(OvertimeApplicationDO application, String actionType, String fromStatus,
String toStatus, String fieldChanges, String reason, String remark) {
BizAuditLogDO auditLog = new BizAuditLogDO();
auditLog.setBizType(OvertimeApplicationConstants.BIZ_TYPE);
auditLog.setBizId(application.getId());
auditLog.setActionType(actionType);
auditLog.setFromStatus(fromStatus);
auditLog.setToStatus(toStatus);
auditLog.setFieldChanges(fieldChanges);
auditLog.setReason(reason);
auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
auditLog.setRemark(StringUtils.hasText(remark) ? remark : buildSnapshotRemark(application));
bizAuditLogMapper.insert(auditLog);
}
private String buildSnapshotRemark(OvertimeApplicationDO application) {
return "申请人:" + defaultText(application.getApplicantName())
+ ",加班日期:" + application.getOvertimeDate()
+ ",加班时长:" + defaultText(application.getOvertimeDuration());
}
private String buildFieldChanges(OvertimeApplicationDO before, OvertimeApplicationDO after) {
Map<String, Object> fieldChanges = new LinkedHashMap<>();
appendFieldChange(fieldChanges, "overtimeDate", valueOf(before, OvertimeApplicationDO::getOvertimeDate),
valueOf(after, OvertimeApplicationDO::getOvertimeDate));
appendFieldChange(fieldChanges, "overtimeDuration",
valueOf(before, OvertimeApplicationDO::getOvertimeDuration),
valueOf(after, OvertimeApplicationDO::getOvertimeDuration));
appendFieldChange(fieldChanges, "overtimeReason", valueOf(before, OvertimeApplicationDO::getOvertimeReason),
valueOf(after, OvertimeApplicationDO::getOvertimeReason));
appendFieldChange(fieldChanges, "overtimeContent", valueOf(before, OvertimeApplicationDO::getOvertimeContent),
valueOf(after, OvertimeApplicationDO::getOvertimeContent));
appendFieldChange(fieldChanges, "approverId", valueOf(before, OvertimeApplicationDO::getApproverId),
valueOf(after, OvertimeApplicationDO::getApproverId));
appendFieldChange(fieldChanges, "approverName", valueOf(before, OvertimeApplicationDO::getApproverName),
valueOf(after, OvertimeApplicationDO::getApproverName));
appendFieldChange(fieldChanges, "statusCode", valueOf(before, OvertimeApplicationDO::getStatusCode),
valueOf(after, OvertimeApplicationDO::getStatusCode));
return fieldChanges.isEmpty() ? null : JsonUtils.toJsonString(fieldChanges);
}
private void appendFieldChange(Map<String, Object> fieldChanges, String fieldName, Object before, Object after) {
if (Objects.equals(before, after)) {
return;
}
Map<String, Object> value = new LinkedHashMap<>();
value.put("before", before);
value.put("after", after);
fieldChanges.put(fieldName, value);
}
private <T> T valueOf(OvertimeApplicationDO application, Function<OvertimeApplicationDO, T> getter) {
return application == null ? null : getter.apply(application);
}
private OvertimeApplicationDO cloneApplication(OvertimeApplicationDO source) {
OvertimeApplicationDO target = new OvertimeApplicationDO();
target.setId(source.getId());
target.setApplicantId(source.getApplicantId());
target.setApplicantName(source.getApplicantName());
target.setOvertimeDate(source.getOvertimeDate());
target.setOvertimeDuration(source.getOvertimeDuration());
target.setOvertimeReason(source.getOvertimeReason());
target.setOvertimeContent(source.getOvertimeContent());
target.setApproverId(source.getApproverId());
target.setApproverName(source.getApproverName());
target.setStatusCode(source.getStatusCode());
target.setApprovalComment(source.getApprovalComment());
target.setSubmitTime(source.getSubmitTime());
target.setApprovalTime(source.getApprovalTime());
return target;
}
private OvertimeApplicationDO mergeUpdated(OvertimeApplicationDO current, OvertimeApplicationDO update) {
OvertimeApplicationDO after = cloneApplication(current);
if (update.getOvertimeDate() != null) {
after.setOvertimeDate(update.getOvertimeDate());
}
if (update.getOvertimeDuration() != null) {
after.setOvertimeDuration(update.getOvertimeDuration());
}
if (update.getOvertimeReason() != null) {
after.setOvertimeReason(update.getOvertimeReason());
}
if (update.getOvertimeContent() != null) {
after.setOvertimeContent(update.getOvertimeContent());
}
if (update.getApproverId() != null) {
after.setApproverId(update.getApproverId());
}
if (update.getApproverName() != null) {
after.setApproverName(update.getApproverName());
}
if (update.getStatusCode() != null) {
after.setStatusCode(update.getStatusCode());
}
after.setApprovalComment(update.getApprovalComment());
if (update.getSubmitTime() != null) {
after.setSubmitTime(update.getSubmitTime());
}
after.setApprovalTime(update.getApprovalTime());
return after;
}
private String normalizeRequiredText(String value, String message) {
if (!StringUtils.hasText(value)) {
throw invalidParamException(message);
}
return value.trim();
}
private String normalizeNullableText(String value) {
if (!StringUtils.hasText(value)) {
return null;
}
return value.trim();
}
private String defaultText(String value) {
return StringUtils.hasText(value) ? value : "";
}
}

View File

@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.util.collection.CollectionUtils;
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
import com.njcn.rdms.module.system.api.user.dto.UserManagementRelationRespDTO;
import com.njcn.rdms.module.system.enums.ApiConstants;
import io.swagger.v3.oas.annotations.Operation;
@@ -33,6 +34,11 @@ public interface UserManagementRelationApi {
@Parameter(name = "subordinateUserId", description = "被管理者用户ID", example = "2", required = true)
CommonResult<List<UserManagementRelationRespDTO>> getRelationListBySubordinateUserId(@RequestParam("subordinateUserId") Long subordinateUserId);
@GetMapping(PREFIX + "/direct-manager")
@Operation(summary = "根据用户ID获得当前生效的直属上级")
@Parameter(name = "userId", description = "用户ID", example = "2", required = true)
CommonResult<AdminUserRespDTO> getDirectManager(@RequestParam("userId") Long userId);
@GetMapping(PREFIX + "/list")
@Operation(summary = "获得管理链路列表")
@Parameter(name = "ids", description = "关系编号数组", example = "1,2", required = true)

View File

@@ -32,4 +32,14 @@ public interface DictTypeConstants {
*/
String RDMS_TASK_ITEM_TYPE="rdms_task_item_type";
/**
* 加班申请审批状态字典。
*/
String RDMS_OVERTIME_APPLICATION_STATUS = "rdms_overtime_application_status";
/**
* 加班申请时长快捷选项字典。
*/
String RDMS_OVERTIME_DURATION = "rdms_overtime_duration";
}

View File

@@ -2,7 +2,9 @@ package com.njcn.rdms.module.system.api.user;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
import com.njcn.rdms.module.system.api.user.dto.UserManagementRelationRespDTO;
import com.njcn.rdms.module.system.dal.dataobject.user.AdminUserDO;
import com.njcn.rdms.module.system.dal.dataobject.user.UserManagementRelationDO;
import com.njcn.rdms.module.system.service.user.UserManagementRelationService;
import io.swagger.v3.oas.annotations.Hidden;
@@ -36,6 +38,15 @@ public class UserManagementRelationApiImpl implements UserManagementRelationApi
return success(BeanUtils.toBean(list, UserManagementRelationRespDTO.class));
}
@Override
public CommonResult<AdminUserRespDTO> getDirectManager(Long userId) {
AdminUserDO manager = userManagementRelationService.getDirectManager(userId);
if (manager == null) {
return success(null);
}
return success(BeanUtils.toBean(manager, AdminUserRespDTO.class));
}
@Override
public CommonResult<List<UserManagementRelationRespDTO>> getRelationList(Collection<Long> ids) {
if (ids == null || ids.isEmpty()) {

View File

@@ -26,6 +26,7 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -157,6 +158,26 @@ public class UserManagementRelationController {
return success(list);
}
/**
* 获得某用户当前生效的直属上级
*
* @param userId 用户ID
* @return 直属上级用户,不存在则返回 null
*/
@GetMapping("/direct-manager")
@Operation(summary = "获得某用户当前生效的直属上级")
@Parameter(name = "userId", description = "用户ID", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('system:user-management-relation:query')")
public CommonResult<UserSimpleRespVO> getDirectManager(@RequestParam("userId") Long userId) {
AdminUserDO manager = userManagementRelationService.getDirectManager(userId);
if (manager == null) {
return success(null);
}
List<Long> deptIds = manager.getDeptId() == null ? Collections.emptyList() : List.of(manager.getDeptId());
Map<Long, DeptDO> deptMap = deptService.getDeptMap(deptIds);
return success(UserConvert.INSTANCE.convertSimpleList(List.of(manager), deptMap).get(0));
}
/**
* 获取未绑定直属上级的候选下级用户列表
* @return 候选下级用户列表

View File

@@ -6,6 +6,7 @@ import com.njcn.rdms.framework.encrypt.core.annotation.ApiEncrypt;
import com.njcn.rdms.module.system.controller.admin.user.vo.profile.UserProfileRespVO;
import com.njcn.rdms.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO;
import com.njcn.rdms.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO;
import com.njcn.rdms.module.system.controller.admin.user.vo.user.UserSimpleRespVO;
import com.njcn.rdms.module.system.convert.user.UserConvert;
import com.njcn.rdms.module.system.dal.dataobject.dept.DeptDO;
import com.njcn.rdms.module.system.dal.dataobject.dept.PostDO;
@@ -17,6 +18,7 @@ import com.njcn.rdms.module.system.service.dept.PostService;
import com.njcn.rdms.module.system.service.permission.PermissionService;
import com.njcn.rdms.module.system.service.permission.RoleService;
import com.njcn.rdms.module.system.service.user.AdminUserService;
import com.njcn.rdms.module.system.service.user.UserManagementRelationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
@@ -26,7 +28,9 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
import static com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@@ -48,6 +52,8 @@ public class UserProfileController {
private PermissionService permissionService;
@Resource
private RoleService roleService;
@Resource
private UserManagementRelationService userManagementRelationService;
@GetMapping("/get")
@Operation(summary = "获得登录用户信息")
@@ -67,6 +73,18 @@ public class UserProfileController {
return success(UserConvert.INSTANCE.convert(user, userRoles, dept, position));
}
@GetMapping("/direct-manager")
@Operation(summary = "获得当前登录用户的直属上级")
public CommonResult<UserSimpleRespVO> getLoginUserDirectManager() {
AdminUserDO manager = userManagementRelationService.getDirectManager(getLoginUserId());
if (manager == null) {
return success(null);
}
List<Long> deptIds = manager.getDeptId() == null ? Collections.emptyList() : List.of(manager.getDeptId());
Map<Long, DeptDO> deptMap = deptService.getDeptMap(deptIds);
return success(UserConvert.INSTANCE.convertSimpleList(List.of(manager), deptMap).get(0));
}
@PutMapping("/update")
@Operation(summary = "修改用户个人信息")
public CommonResult<Boolean> updateUserProfile(@Valid @RequestBody UserProfileUpdateReqVO reqVO) {

View File

@@ -51,6 +51,7 @@ public class DictDataDO extends BaseDO {
*
* 对应到 element-ui 为 default、primary、success、info、warning、danger
*/
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private String colorType;
/**
* css 样式

View File

@@ -73,4 +73,23 @@ public interface UserManagementRelationMapper extends BaseMapperX<UserManagement
return selectList(UserManagementRelationDO::getSubordinateUserId, subordinateUserId);
}
/**
* 根据被管理者用户ID查询当前生效的上级关系列表
*
* @param subordinateUserId 被管理者用户ID
* @return 当前生效的用户管理链路DO列表
*/
default List<UserManagementRelationDO> selectValidListBySubordinateUserId(Long subordinateUserId) {
LocalDateTime now = LocalDateTime.now();
return selectList(new LambdaQueryWrapperX<UserManagementRelationDO>()
.eq(UserManagementRelationDO::getSubordinateUserId, subordinateUserId)
// (from IS NULL OR from <= now)
.and(w -> w.isNull(UserManagementRelationDO::getEffectiveFrom)
.or().le(UserManagementRelationDO::getEffectiveFrom, now))
// (until IS NULL OR until >= now)
.and(w -> w.isNull(UserManagementRelationDO::getEffectiveUntil)
.or().ge(UserManagementRelationDO::getEffectiveUntil, now))
.orderByDesc(UserManagementRelationDO::getId));
}
}

View File

@@ -89,6 +89,14 @@ public interface UserManagementRelationService {
*/
List<UserManagementRelationDO> getRelationListBySubordinateUserId(Long subordinateUserId);
/**
* 获得某用户当前生效的直属上级
*
* @param userId 用户ID
* @return 直属上级用户,不存在则返回 null
*/
AdminUserDO getDirectManager(Long userId);
/**
* 获得用户管理链路树形结构
*

View File

@@ -270,7 +270,33 @@ public class UserManagementRelationServiceImpl implements UserManagementRelation
*/
@Override
public List<UserManagementRelationDO> getRelationListBySubordinateUserId(Long subordinateUserId) {
return userManagementRelationMapper.selectListBySubordinateUserId(subordinateUserId);
if (subordinateUserId == null) {
return Collections.emptyList();
}
return userManagementRelationMapper.selectValidListBySubordinateUserId(subordinateUserId);
}
/**
* 获得某用户当前生效的直属上级
*
* @param userId 用户ID
* @return 直属上级用户,不存在则返回 null
*/
@Override
public AdminUserDO getDirectManager(Long userId) {
List<UserManagementRelationDO> relations = getRelationListBySubordinateUserId(userId);
if (CollUtil.isEmpty(relations)) {
return null;
}
Long managerUserId = relations.get(0).getManagerUserId();
if (managerUserId == null) {
return null;
}
AdminUserDO manager = adminUserService.getUser(managerUserId);
if (!adminUserService.isUserAvailable(manager)) {
return null;
}
return manager;
}
/**